I have a multi-tenant Symfony 5 application that consists in a general database which is responsible with global data and a dynamic amount of tenant databases, using the same schema among them, that are responsible with tenant specific data storage.
In total I have 2 entity managers, default that is connected to the general database and tenant that uses a wrapper (MultiDbConnectionWrapper) that can switch between any of the tenants databases.
config/doctrine.yaml
doctrine:
dbal:
connections:
default:
url: '%env(resolve:DATABASE_URL_CORE)%'
tenant:
url: '%env(resolve:DATABASE_URL_TENANT)%'
wrapper_class: App\Dbal\MultiDbConnectionWrapper
orm:
auto_generate_proxy_classes: true
entity_managers:
default:
connection: default
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
mappings:
Core:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity/Core'
prefix: 'App\Entity\Core'
alias: Core
tenant:
connection: default
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
mappings:
Tenant:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity/Tenant'
prefix: 'App\Entity\Tenant'
alias: tenant
For each EM, I'm using a different directory for migrations as well as a different migration config file/
config/migrations/tenant_migrations.yaml
migrations_paths:
'TenantMigrations': 'migrations/tenant'
em: 'tenant'
config/migrations/core_migrations.yaml
migrations_paths:
'CoreMigrations': 'migrations/core'
em: 'default'
My issue is that I'm trying to create a command that will cycle through all of the tenants databases and will run the migrations on each one. In my main database I have a table called tenants where I have stored the name of that tenant database.
To do so, I created the following command:
src/Command/MigrateTenantsCommand.php
protected function execute(InputInterface $input, OutputInterface $output): int
{
$tenants = $this->tenantRepository->findAll();
$connection = $this->tenantEntityManager->getConnection();
foreach ($tenants as $tenant){
$db = $tenant->getDatabaseUuid();
$connection->selectDatabase($db);
$this->getApplication()
->find('doctrine:migrations:migrate')
->run(new ArrayInput([
'--configuration' => 'config/migrations/tenant_migrations.yaml',
'--no-interaction' => true
]),$output);
}
return Command::SUCCESS;
}
The problem is that if I have at least 2 tenants stored in database and the command doctrine:migrations:migrate has to run twice, at the second run it throws the following error:
In FrozenDependencies.php line 13:
The dependencies are frozen and cannot be edited anymore.
From what I've tried it seems that somehow it is not related to the fact that I'm changing databases at $connection->selectDatabase($db);, but the fact that I cannot run twice the doctrine:migrations:migrate inside a command.
I've tried running also the following command and I received the same error:
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->getApplication()
->find('doctrine:migrations:migrate')
->run(new ArrayInput([
'--configuration' => 'config/migrations/core_migrations.yaml'
]),$output);
$this->getApplication()
->find('doctrine:migrations:migrate')
->run(new ArrayInput([
'--configuration' => 'config/migrations/core_migrations.yaml'
]),$output);
return Command::SUCCESS;
}
Anyone got any ideea why this happens? Thank you!
How I solved the issue:
It seems that doctrine:migrations:migrate locks itself in order to prevent multiple runs from the same command.
In order to bypass this, we can run the command inside a different process at each loop.
protected function execute(InputInterface $input, OutputInterface $output): int
{
$companies = $this->companyRepository->findAll();
foreach ($companies as $company){
$db = $company->getDatabaseUuid();
$connection = $this->tenantEntityManager->getConnection();
$connection->selectDatabase($db);
$process = new Process([
"bin/console",
"doctrine:migrations:migrate",
"--configuration=config/migrations/tenant_migrations.yaml",
"--em=tenant",
]);
$process->run();
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
echo $process->getOutput();
}
return Command::SUCCESS;
}
This will lead to a second problem.
Since we run $connection->selectDatabase($db); in the current command process, but doctrine:migrations:migrate inside a new process, the entity manager won't have the database selected.
The solution was to create 2 commands as follows:
migrate:single-tenant command that can recieve a --db parameter and runs doctrine:migrations:migrate the default way
migrate:tenants command that get all the databases and run migrate:single-tenant --db=$db as a new process in a loop
This way we set the database on the same process as the execution of doctrine:migrations:migrate and in the same time we keep only one migration per process
Here are the final commands
migrate:single-tenant
protected function configure(): void
{
$this
->addOption('db', null, InputOption::VALUE_REQUIRED, 'Database selection')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$db = $input->getOption('db');
if (!$db) {
$io->error('"--db" option missing');
return Command::FAILURE;
}
$connection = $this->tenantEntityManager->getConnection();
$connection->selectDatabase($db);
$this->getApplication()
->find('doctrine:migrations:migrate')
->run(new ArrayInput([
'--configuration' => 'config/migrations/tenant_migrations.yaml',
'--em' => 'tenant'
]),$output);
$io->success("$db migrated succesfully");
return Command::SUCCESS;
}
migrate:tenants
protected function execute(InputInterface $input, OutputInterface $output): int
{
$companies = $this->companyRepository->findAll();
foreach ($companies as $company){
$db = $company->getDatabaseUuid();
$process = new Process([
"bin/console",
"migrate:single-tenant",
"--db=$db",
]);
$process->run();
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
echo $process->getOutput();
}
return Command::SUCCESS;
}
Related
PHPUnit 7.5.15 by Sebastian Bergmann and contributors.
Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException : You have requested a non-existent service "test.service_container". Did you mean this: "service_container"?
/opt/project/backend/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Container.php:277
/opt/project/backend/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Container.php:225
/opt/project/backend/tests/Functional/KernelAwareTest.php:49
/opt/project/backend/tests/Functional/FlightTaskManagement/AssignEmployeeToTaskTest.php:47
public function setUp(): void
{
global $kernel;
$this->kernel = TestKernel::get();
$kernel = $this->kernel;
$container = $this->kernel->getContainer();
if ($container === null)
throw new \InvalidArgumentException('Container can not be null.');
$this->container = $container->get('test.service_container');
// $this->container = $container->get('service_container');
/** #var Registry $doctrine */
$doctrine = $this->container->get('doctrine');
/** #var \Doctrine\ORM\EntityManager $manager */
$manager = $doctrine->getManager();
$this->entityManager = $manager;
$this->entityManager->beginTransaction();
if (!$this->container->initialized(WorkDistributionTransport::class)) {
$this->container->set(WorkDistributionTransport::class, new InMemoryTransport());
}
if (!$this->container->initialized(Configuration::class)) {
$this->container->set(Configuration::class, new TestConfiguration());
}
parent::setUp();
}
It fails at line
$this->container = $container->get('test.service_container');
Symfony is 4.1 but looks like not finished to update. I can't remember by what we decided that it was not finished to update from earlier version.
Not clear if that is the problem that it is not finished to update. Looks like in 4.0 there is no such service so thats why. But then how to make it appear here?
Or maybe I can use
$this->container = $container->get('service_container');
as with earlier versions? Just what is faster way?
I just tried using
$this->container = $container->get('service_container');
but I then get
Doctrine\DBAL\DBALException : An exception occured while establishing a connection to figure out your platform version.
You can circumvent this by setting a 'server_version' configuration value
But I had set the version in config_test.yml so not clear which way is faster to fix.
doctrine:
dbal:
server_version: 5.7
driver: pdo_mysql
host: db_test
port: null
dbname: project
user: project
password: project
Probably if I load service_container then it does not load test config and thats why I get this server_version error. So then need to somehow make it load test config.
Found: I had hardcoded dev environment in AppKernel. Probably thats why I was getting this error. Hardcoding test env fixed. Would be good somehow to make it without hardcoding, but it is still better than nothing:
public function __construct($environment, $debug)
{
// $environment ='dev';
$environment ='test';
$debug= true;
parent::__construct($environment, $debug);
date_default_timezone_set('UTC');
}
I am migrating a Silex app to Symfony Flex and everything is working so far, except that when I run the phpunit tests I get the response body output into the phpunit output.
ie.
> bin/phpunit
#!/usr/bin/env php
PHPUnit 6.5.13 by Sebastian Bergmann and contributors.
Testing unit
.......<http://data.nobelprize.org/resource/laureate/914> a <http://data.nobelprize.org/terms/Laureate> , <http://xmlns.com/foaf/0.1/Person> ;
<http://www.w3.org/2000/01/rdf-schema#label> "Malala Yousafzai" ;
<http://data.nobelprize.org/terms/laureateAward> <http://data.nobelprize.org/resource/laureateaward/974> ;
<http://data.nobelprize.org/terms/nobelPrize> <http://data.nobelprize.org/resource/nobelprize/Peace/2014> ;
the entire RDF document then
. 8 / 8 (100%)
Time: 1.07 seconds, Memory: 14.00MB
OK (8 tests, 71 assertions)
Generating code coverage report in Clover XML format ... done
So it is working fine, but I can't figure out how to disable this output?
The request is simply
$this->client->request('GET', "/nobel_914.ttl", [], [], ['HTTP_ACCEPT' => $request_mime]);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), "GET should be allowed.");
$response = $this->client->getResponse();
$charset = $response->getCharset();
etc.
and the client is setup in a base class like this
class MyAppTestBase extends WebTestCase
{
/**
* #var \Symfony\Component\BrowserKit\Client
*/
protected $client;
/**
* {#inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->client = static::createClient();
$this->client->catchExceptions(false);
}
I'm sure I'm missing something obvious but this is new to me. I am running in the 'test' environment and with 'debug' == false.
Any help appreciated.
So this was probably a problem all along but just started being exposed in the switch from Silex to Symfony Flex.
We were streaming responses via
$filename = $this->path;
$stream = function () use ($filename) {
readfile($filename);
};
return new StreamedResponse($stream, 200, $res->headers->all());
and the readfile was throwing the content to the output buffer. Switching the readfile to file_get_contents resolved this
$filename = $this->path;
$stream = function () use ($filename) {
file_get_contents($filename);
};
return new StreamedResponse($stream, 200, $res->headers->all());
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', '/');
// ...
}
}
I am trying to behat my application and I have a big problem; DB tables are not created so I can't put any fixtures.
My scenario is:
Scenario: Check the stories page
Given Database is set
And I am logged as "admin" and password "123123123"
And print last response
...
Part of FeatureContext:
/**
* #Given /^Database is set$/
*/
public function databaseIsSet()
{
$this->generateSchema() ;
$admin = new User() ;
$admin->setRoles(array(User::ROLE_SUPER_ADMIN)) ;
$admin->setEnabled(true) ;
$admin->setUsername("admin") ;
$admin->setPlainPassword("123123123") ;
$admin->setEmail("admin#mysite.com") ;
$em = $this->getEntityManager() ;
$em->persist($admin) ;
$em->flush() ;
echo $admin->getId() . "==" ;
echo "db set" ;
}
/**
* #Given /^I am logged as "([^"]*)" and password "([^"]*)"$/
*/
public function iAmLoggedAsAndPassword($username, $password)
{
return array(
new Step\When('I am on "/login"'),
new Step\When('I fill in "username" with "' . $username . '"'),
new Step\When('I fill in "password" with "' . $password . '"'),
new Step\When('I press "Login"'),
);
}
protected function generateSchema()
{
// Get the metadatas of the application to create the schema.
$metadatas = $this->getMetadatas();
if ( ! empty($metadatas)) {
/**
* #var \Doctrine\ORM\Tools\SchemaTool
*/
$tool = new SchemaTool($this->getEntityManager());
// $tool->dropDatabase() ;
$tool->createSchema($metadatas);
} else {
throw new Doctrine\DBAL\Schema\SchemaException('No Metadata Classes to process.');
}
}
/**
* Overwrite this method to get specific metadatas.
*
* #return Array
*/
protected function getMetadatas()
{
$result = $this->getEntityManager()->getMetadataFactory()->getAllMetadata() ;return $result;
}
protected function getEntityManager()
{
return $this->kernel->getContainer()->get("doctrine")->getEntityManager() ;
}
....
The code for generateSchema is taken somewhere from internet and used in Phpunits tests I have and works perfectly.
But; when I run bin/behat, I get
SQLSTATE[HY000]: General error: 1 no such table: tbl_user
after login part of scenario.
The echo statement I have is also shown in output, just to make sure the method is actually executed. Also, $admin gets an ID of 1 which is also visible in output.
My test env is using default sqlite DB, and it is irrelevant if I put 'http://mysite.local/app_dev.php/' or 'http://mysite.local/app_test.php/' for base_url in config; the login doesn't work although I copy&pasted it from knpLabs page. To make sure $admin is still in DB, I tried to reload it from repository and it works (I removed that part of code).
Help?
Actually, I found the problem. Sqlite works in-memory and upon each request to some page like login url, the previous state had been lost. I created new enviroment app_behat.php with these setting in config_behat.yml:
imports:
- { resource: config.yml }
framework:
test: ~
session:
storage_id: session.storage.mock_file
doctrine:
dbal:
dbname: other_database
and it works now. Maybe someone will find this usefull.
I had the same problem and for me the problem was in config_test.yml file.
I changed pdo_sqlite to pdo_mysql.
doctrine:
dbal:
driver: pdo_mysql
# driver: pdo_sqlite
And it works like a charm.
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