In migration class depending on logic, I need to use different types of database connections. How in migration class to get new connection by connection name?
Currently in doctrine.yaml file I have connection names "default", "user", "admin" and "cron".
My migration class:
final class Version20190711123152 extends AbstractMigration
{
public function up(Schema $schema) : void
{
...
if($someCondition) {
$this->setConnection($wantedConnection) // how to set $wantedConnection for example on "admin" connection
}
}
/**
* #param Connection $connection
*/
public function setConnection(Connection $connection): void
{
$this->connection = $connection;
}
I am using Symfony 4.3
I don't know your exact use case, but I don't think the migrations should be conditional, i.e. you may end up with inconsistent databases across different environments.
Maybe consider storing migration files in separate directories and use different configuration and entity manager when running migrations.
# /config/migrations/default.yaml
name: "Default Migrations"
migrations_namespace: "App\Migrations\Default"
table_name: "doctrine_migration_versions"
migrations_directory: "src/Migrations/Default"
# /config/migrations/admin.yaml
name: "Admin Migrations"
migrations_namespace: "App\Migrations\Admin"
table_name: "doctrine_migration_versions"
migrations_directory: "src/Migrations/Admin"
Assuming you have configured two entity managers - one default and one i.e. with "admin" name you can run those migrations separately:
php bin/console doctrine:migrations:migrate --configuration=config/migrations/default.yaml
php bin/console doctrine:migrations:migrate --configuration=config/migrations/admin.yaml --em=admin
Related
I have the following UserDataPersister (taken straight from the tutorial) configured:
Information for Service "App\DataPersister\UserDataPersister"
=============================================================
Service ID App\DataPersister\UserDataPersister
Class App\DataPersister\UserDataPersister
Tags api_platform.data_persister (priority: -1000)
Public no
Shared yes
Abstract no
Autowired yes
Autoconfigured yes
and the following User fixture:
App\Entity\User:
user_{1..10}:
email: "usermail_<current()>\\#email.org"
plainPassword: "plainPassword_<current()>"
__calls:
- initUuid: []
But I get errors when loading this fixture:
An exception occurred while executing 'INSERT INTO "user" (id, uuid, roles, password, email) VALUES (?, ?, ?, ?, ?)' with params [281, "16ac40d3-53af-45dc-853f-e26f188d
1818", "[]", null, "usermail1#email.org"]:
SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "password" of relation "user" violates not-null constraint
DETAIL: Failing row contains (281, 16ac40d3-53af-45dc-853f-e26f188d1818, [], null, usermail1#email.org).
My implementation of UserDataPersister is identical with this.
Quote from Article at the end
If we stopped now... yay! We haven't... really... done anything: we
added this new plainPassword property... but nothing is using it! So,
the request would ultimately explode in the database because our
$password field will be null.
Next, we need to hook into the request-handling process: we need to
run some code after deserialization but before persisting. We'll do
that with a data persister.
Since unit test would POST the request, the data persistor is called by api-platform and it will pick up encoding logic by event. In case of fixtures, direct doctrine batch insert is done, this will bypass all persistence logic and would result in null password.
There is a way to solve this as mentioned by #rishta Use Processor to implement hash to your data fixtures as referenced in Documentation
<?php
namespace App\DataFixtures\Processor;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Fidry\AliceDataFixtures\ProcessorInterface;
use App\Entity\User;
final class UserProcessor implements ProcessorInterface
{
private $userPasswordEncoder;
public function __construct(EntityManagerInterface $entityManager, UserPasswordEncoderInterface $userPasswordEncoder) {
$this->userPasswordEncoder = $userPasswordEncoder;
}
/**
* #inheritdoc
*/
public function preProcess(string $fixtureId, $object): void {
if (false === $object instanceof User) {
return;
}
$object = $this->userPasswordEncoder(
$object,
$object->getPlainPassword()
);
}
/**
* #inheritdoc
*/
public function postProcess(string $fixtureId, $object): void
{
// do nothing
}
}
Register service :
# app/config/services.yml
services:
_defaults:
autoconfigure: true
App\DataFixtures\Processor\UserProcessor: ~
#add tag in case autoconfigure is disabled, no need for auto config
#tags: [ { name: fidry_alice_data_fixtures.processor } ]
One of the better ways to do input masking in API Platform is to use DTO Pattern as oppose to suggested by article, in which you are allowed to :
Create separate input & output data objects
Transform Underlying date to and from the objects
Choose Different IO objects for each operation whenever needed
More on DTO in documentation
I'm trying to get a simple "200 Response" test to work for a part of a website requiring an authenticated user. I think I've got the creation of the Session working, as during debugging the Controller function is called and a User is retrieved (using $this->getUser()).
However, afterwards the function fails with the following message:
1) App\Tests\Controller\SecretControllerTest::testIndex200Response
expected other status code for 'http://localhost/secret_url/':
error:
Multiple non-persisted new entities were found through the given association graph:
* A new entity was found through the relationship 'App\Entity\User#role' that was not configured to cascade persist operations for entity: ROLE_FOR_USER. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade
persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}).
* A new entity was found through the relationship 'App\Entity\User#secret_property' that was not configured to cascade persist operations for entity: test123. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade pe
rsist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). (500 Internal Server Error)
Failed asserting that 500 matches expected 200.
This would make sense if this was not already stored in the (MySQL) database and retrieved with Doctrine. The records are created using Fixtures on each run/for each test. This is why in the Controller $this->getUser() functions as expected.
The test I'm wanting to work:
public function testIndex200Response(): void
{
$client = $this->getAuthenticatedSecretUserClient();
$this->checkPageLoadResponse($client, 'http://localhost/secret_url/');
}
Get a user:
protected function getAuthenticatedSecretUserClient(): HttpKernelBrowser
{
$this->loadFixtures(
[
RoleFixture::class,
SecretUserFixture::class,
]
);
/** #var User $user */
$user = $this->entityManager->getRepository(User::class)->findOneBy(['username' => 'secret_user']);
$client = self::createClient(
[],
[
'PHP_AUTH_USER' => $user->getUsername(),
'PHP_AUTH_PW' => $user->getPlainPassword(),
]
);
$this->createClientSession($user, $client);
return $client;
}
Create a session:
// Based on https://symfony.com/doc/current/testing/http_authentication.html#using-a-faster-authentication-mechanism-only-for-tests
protected function createClientSession(User $user, HttpKernelBrowser $client): void
{
$authenticatedGuardToken = new PostAuthenticationGuardToken($user, 'chain_provider', $user->getRoles());
$tokenStorage = new TokenStorage();
$tokenStorage->setToken($authenticatedGuardToken);
$session = self::$container->get('session');
$session->set('_security_<security_context>', serialize($authenticatedGuardToken));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$client->getCookieJar()->set($cookie);
self::$container->set('security.token_storage', $tokenStorage);
}
This works for the creating of the client, session and cookie.
When the Request is executed to the $url in the first function, it gets into the endpoint, confirming the User is indeed authenticated.
According to the documentation here a User should be "refreshed" from via the configured provider (using Doctrine in this case) to check if a given object matches a stored object.
[..] At the beginning of the next request, it's deserialized and then passed to your user provider to "refresh" it (e.g. Doctrine queries for a fresh user).
I would expect this would also ensure that the session User is replaced with a Doctrine managed User object to prevent the error above.
How can I go about solving that the User in the session becomes a managed User during PhpUnit testing?
(Note: the production code works without any issue, this problem only arises during testing (legacy code now starting to get tests))
Ok, had multiple issues, but got it working doing the following:
First, was creating a Client using incorrect password, I was creating (in Fixtures) User entities with username and password being identical. The function getPlainPassword, though present in an interface, was not something stored, so was a blank value.
Corrected code:
$client = self::createClient(
[],
[
'PHP_AUTH_USER' => $user->getUsername(),
'PHP_AUTH_PW' => $user->getUsername(),
]
);
Next, a User not being refreshed took some more.
In config/packages/security.yaml, add the following:
security:
firewalls:
test:
security: ~
This is to create the "test" key, as creating that immediately in the next file will cause a permission denied error. In config/packages/test/security.yaml, create the following:
security:
providers:
test_user_provider:
id: App\Tests\Functional\Security\UserProvider
firewalls:
test:
http_basic:
provider: test_user_provider
This adds a custom UserProvider specifically for testing purposes (hence usage App\Tests\ namespace). You must register this service in your config/services_test.yaml:
services:
App\Tests\Functional\Security\:
resource: '../tests/Functional/Security'
Not sure you'll need it, but I added in config/packages/test/routing.yaml the following:
parameters:
protocol: http
As PhpUnit is testing via CLI, there by default is no secure connection, can vary by environment so see if you need it.
Lastly, config for test framework in config/packages/test/framework.yaml:
framework:
test: true
session:
storage_id: session.storage.mock_file
All of the above config (apart from the http bit) is to ensure that a custom UserProvider will be used to provider User objects during testing.
This might excessive for others, but our setup (legacy) has some custom work for providing Users for authentication (which seems very related but far out of my current issue scope).
Back on to the UserProvider, it's setup like so:
namespace App\Tests\Functional\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class UserProvider implements UserProviderInterface
{
/** #var UserRepository */
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function loadUserByUsername($username)
{
try {
return $this->userRepository->getByUsername($username);
} catch (UserNotFoundException $e) {
throw new UsernameNotFoundException("Username: $username unknown");
}
}
public function refreshUser(UserInterface $user)
{
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return User::class === $class;
}
}
Note: should you use this, you need to have a getByUsername function in your UserRepository.
Please note, this might not be the solution for you. Maybe you need to change it up, maybe it's completely off. Either way, thought to leave a solution for any future souls.
I want to test a service who send mail. I have create a unit test but i have some deprecation warning and i want to know the good use.
In my setUp() function i get the service like this
$this->container = self::$kernel->getContainer();
$this->swiftMailer = $this->container->get('swiftmailer.mailer');
But i have this message
The "swiftmailer.mailer" service is private, getting it from the container is deprecated since Symfony 3.2 and will fail in 4.0. You should either make the service public, or stop using the container directly and use dependency injection instead.
What is the best pratice to do ?
I have the same message for security.authentication.manager
Services were made private by default in Symfony 3.4.
Symfony 4.1
Starting with Symfony 4.1 all private services are made available in test environment via a special test container:
class FooTest extends KernelTestCase
{
static::bootKernel();
$this->swiftmailer = static::$container->get('swiftmailer.mailer');
}
Symfony 3.4 and 4.0
One way you could solve it in Symfony 3.4 and 4.0 is to register a service locator in test environment, that would expose private services you need access to in tests.
Another way would be to simply create a public alias for each private service you need access to in tests.
For example:
# app/config/config_test.yml
services:
test_alias.swiftmailer.mailer:
alias: '#swiftmailer.mailer'
public: true
In your test you'll be now able to access your private service via the public alias test_alias.swiftmailer.mailer:
$this->container = self::$kernel->getContainer();
$this->swiftMailer = $this->container->get('test_alias.swiftmailer.mailer');
This approach with all its pros/cons is described in this post with code examples.
There is no need to extend and maintain extra configs lines for tests. There should be no public: true in them:
The best solution to access private services is to add a Compiler Pass that makes all services public for tests.
1. Update Kernel
use Symfony\Component\HttpKernel\Kernel;
+use Symplify\PackageBuilder\DependencyInjection\CompilerPass\PublicForTestsCompilerPass;
final class AppKernel extends Kernel
{
protected function build(ContainerBuilder $containerBuilder): void
{
$containerBuilder->addCompilerPass('...');
+ $containerBuilder->addCompilerPass(new PublicForTestsCompilerPass());
}
}
2. Require or create own Compiler Pass
Where PublicForTestsCompilerPass looks like:
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class PublicForTestsCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $containerBuilder): void
{
if (! $this->isPHPUnit()) {
return;
}
foreach ($containerBuilder->getDefinitions() as $definition) {
$definition->setPublic(true);
}
foreach ($containerBuilder->getAliases() as $definition) {
$definition->setPublic(true);
}
}
private function isPHPUnit(): bool
{
// defined by PHPUnit
return defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__');
}
}
To use this class, just add the package by:
composer require symplify/package-builder
But of course, the better way is to use own class, that meets your needs (you might Behat for tests etc.).
Then all your tests will keep working as expected!
Let me know, how that works for you.
I am starting to work with services in Symfony and therefore created the example service from the symfony documentation:
namespace AppBundle\Service;
use Psr\Log\LoggerInterface;
class MessageGenerator
{
private $logger;
public function __construct(LoggerInterface $logger){
}
public function getMessage()
{
$this->logger->info('Success!');
}
}
I call that service in my controller (I also have the use Statement:
: use AppBundle\Service\MessageGenerator;
$messageGenerator = $this->get(MessageGenerator::class);
$message = $messageGenerator->getMessage();
$this->addFlash('success', $message);
My service is defined in the services.yml file:
app.message_generator:
class: AppBundle\Service\MessageGenerator
public: true
so in my eyes I did everything exactly as described in the documentation and when calling:
php app/console debug:container app.message_generator
in my commandline I get my service:
Option Value
------------------ ------------------------------------
Service ID app.message_generator
Class AppBundle\Service\MessageGenerator
Tags -
Scope container
Public yes
Synthetic no
Lazy no
Synchronized no
Abstract no
Autowired no
Autowiring Types -
Now when I execute the controller function where I call my service I still get the error:
You have requested a non-existent service "appbundle\service\messagegenerator".
Any ideas?
Symfony is a bit confusing at naming: you retrieve the service by requesting it by its defined name: app.message_generator.
$messageGenerator = $this->get('app.message_generator');
Symfony has recently suggested switching from a give-name (app.message_generator) that you are defining the service as, to the class name (AppBundle\Service\MessageGenerator). They are both just 'a name' to call the service.
You are trying to use both, when only the given name is defined.
In the long term, it's suggested to use the ::class based name, and quite possibly allow the framework to find the classes itself, and configure them itself too. This means that, by default, all services are private, and are handled by the framework & it's service container.
In the meantime, while you are learning, you can either:
$messageGenerator = $this->get('app.message_generator');
or define explicitly define the service, and make it public, so it can be fetched with ->get(...) from the container.
# services.yml
AppBundle\Service\MessageGenerator:
class: AppBundle\Service\MessageGenerator
public: true
# php controller
$messageGenerator = $this->get(MessageGenerator::class);
or just injected automatically into the controller, when that is requested
public function __construct(LoggerInterface $logger, MessageGenerator $msgGen)
{
$this->messageGenerator = $msgGen;
}
public function getMessage()
{
$result = $this->messageGenerator->do_things(....);
$this->logger->info('Success!');
}
I followed sitepoints Testing Symfony Apps with a Disposable Database Tutorial.
I added Fixtures in my Testcase and no Errors appear during SetUp. If i add an Error in the Fixtures (e.g. leaving a nullable=false field empty) the Error is shown, so this code does definitely get executed.
My Config:
doctrine:
dbal:
default_connection: memory
connections:
memory:
driver: pdo_sqlite
memory: true
charset: UTF8
My SetUp in my WebTestCase:
protected function setUp() {
parent::setUp();
self::bootKernel();
DatabasePrimer::prime(self::$kernel);
$this->loadFixtures([
'AppBundle\DataFixtures\ORM\UserData',
'AppBundle\DataFixtures\ORM\ArtistData'
]);
}
Yet, in my WebTestCase it appears that no Tables exist.
The output throws a Doctrine Exception saying my table does not exist.
SQLSTATE[HY000]: General error: 1 no such table: my_user_table
If i switch to sql_lite in a file, everything works fine without any other changes:
dbal:
default_connection: file
connections:
file:
driver: pdo_sqlite
path: %kernel.cache_dir%/test.db
charset: UTF8
Anyone had success with said tutorial or using a sqlite memory db for unit tests and has any hints or ideas?
Update:
I changed my Setup to this to ensure the kernel is not shut down in between. It did not help:
parent::setUp();
$this->client = $this->getClient();
MemoryDbPrimer::prime(self::$kernel);
$this->loadFixtures([
'AppBundle\DataFixtures\ORM\UserData',
'AppBundle\DataFixtures\ORM\ArtistData'
]);
When you
$client->request(<METHOD>, <URL>);
which calls
Symfony\Bundle\FrameworkBundleClient::doRequest($request)
After the request the kernel is shutdown by default, and your in-memory database is trashed.
If you call
client->disableReboot();
in the setup() function of your test, this will behavior is disabled, and you can run the whole suite.
I assume you call createClient() in your test functions. The very first thing that createClient() does is call static::bootKernel(). This basically means that the kernel you booted in your setUp() gets shut down and a new kernel is booted, with a fresh instance of the memory SQLite database.
You can move the createClient() call into your setUp(), replacing the bootKernel(), to avoid this:
class MyTest extends WebTestCase
{
private $client = null;
public function setUp()
{
$this->client = static::createClient();
// prime database
}
public function testSomething()
{
$crawler = $this->client->request('GET', '/');
// ...
}
}