Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate db tool and db backup/restore services for version 2.x #398

Merged
merged 16 commits into from
Mar 6, 2018
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,18 @@ Tips for Fixture Loading Tests
cache_sqlite_db: true
```
3. Load your Doctrine fixtures in your tests:
3. For create custom database cache service
**Attention: Don't work with `liip_functional_test.cache_sqlite_db` use only `liip_functional_test.cache_db.*`.**
```yaml
# app/config/config_test.yml
liip_functional_test:
cache_db:
sqlite: liip_functional_test.database_backup.sqllite
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sqlite ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's typo

mysql: liip_functional_test.services_database_backup.mysql_custom
```
4. Load your Doctrine fixtures in your tests:
```php
use Liip\FunctionalTestBundle\Test\WebTestCase;
Expand All @@ -339,7 +350,7 @@ Tips for Fixture Loading Tests
}
```
4. If you don't need any fixtures to be loaded and just want to start off with
5. If you don't need any fixtures to be loaded and just want to start off with
an empty database (initialized with your schema), you can simply call
`loadFixtures` without any argument.
Expand All @@ -359,7 +370,7 @@ Tips for Fixture Loading Tests
}
```
5. Given that you want to exclude some of your doctrine tables from being purged
6. Given that you want to exclude some of your doctrine tables from being purged
when loading the fixtures, you can do so by passing an array of tablenames
to the `setExcludedDoctrineTables` method before loading the fixtures.
Expand All @@ -379,7 +390,7 @@ Tips for Fixture Loading Tests
}
```
6. If you want to append fixtures instead of clean database and load them, you have
7. If you want to append fixtures instead of clean database and load them, you have
to consider use the second parameter $append with value true.
```php
Expand All @@ -398,7 +409,7 @@ Tips for Fixture Loading Tests
}
```
7. This bundle uses Doctrine ORM by default. If you are using another driver just
8. This bundle uses Doctrine ORM by default. If you are using another driver just
specify the service id of the registry manager:
```php
Expand Down
9 changes: 9 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ public function getConfigTreeBuilder(): TreeBuilder
$rootNode
->children()
->booleanNode('cache_sqlite_db')->defaultFalse()->end()
->arrayNode('cache_db')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we then remove cache_sqlite_db ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll remove it.
#398 (comment)

->addDefaultsIfNotSet()
->ignoreExtraKeys(false)
->children()
->scalarNode('sqlite')
->defaultNull()
->end()
->end()
->end()
->scalarNode('command_verbosity')->defaultValue('normal')->end()
->booleanNode('command_decoration')->defaultTrue()->end()
->arrayNode('query')
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/LiipFunctionalTestExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public function load(array $configs, ContainerBuilder $container): void
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('commands.xml');
$loader->load('functional_test.xml');
$loader->load('database_tools.xml');
if (interface_exists('Symfony\Component\Validator\Validator\ValidatorInterface')) {
$loader->load('validator.xml');
}
Expand Down
55 changes: 55 additions & 0 deletions src/Resources/config/database_tools.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="liip_functional_test.services.fixtures_loader_factory" class="Liip\FunctionalTestBundle\Services\FixturesLoaderFactory" public="true">
<argument type="service" id="service_container" />
</service>

<service id="liip_functional_test.services_database_backup.sqlite" class="Liip\FunctionalTestBundle\Services\DatabaseBackup\SqliteDatabaseBackup" public="true">
<argument type="service" id="service_container" />
<argument type="service" id="liip_functional_test.services.fixtures_loader_factory" />
</service>

<service id="liip_functional_test.services_database_backup.mysql_custom" class="Liip\FunctionalTestBundle\Services\DatabaseBackup\MysqlCustomDatabaseBackup" public="true">
<argument type="service" id="service_container" />
<argument type="service" id="liip_functional_test.services.fixtures_loader_factory" />
</service>

<service id="liip_functional_test.services_database_tools.ormdatabase_tool" class="Liip\FunctionalTestBundle\Services\DatabaseTools\ORMDatabaseTool" public="false">
<argument type="service" id="service_container" />
<argument type="service" id="liip_functional_test.services.fixtures_loader_factory" />
</service>
<service id="liip_functional_test.services_database_tools.ormsqllite_database_tool" class="Liip\FunctionalTestBundle\Services\DatabaseTools\ORMSqlliteDatabaseTool" public="false">
<argument type="service" id="service_container" />
<argument type="service" id="liip_functional_test.services.fixtures_loader_factory" />
</service>
<service id="liip_functional_test.services_database_tools.mongo_dbdatabase_tool" class="Liip\FunctionalTestBundle\Services\DatabaseTools\MongoDBDatabaseTool" public="false">
<argument type="service" id="service_container" />
<argument type="service" id="liip_functional_test.services.fixtures_loader_factory" />
</service>
<service id="liip_functional_test.services_database_tools.phpcrdatabase_tool" class="Liip\FunctionalTestBundle\Services\DatabaseTools\PHPCRDatabaseTool" public="false">
<argument type="service" id="service_container" />
<argument type="service" id="liip_functional_test.services.fixtures_loader_factory" />
</service>
<service id="liip_functional_test.services.database_tool_collection" class="Liip\FunctionalTestBundle\Services\DatabaseToolCollection" public="true">
<argument type="service" id="service_container" />
<argument type="service" id="liip_functional_test.services.fixtures_loader_factory" />
<call method="add">
<argument type="service" id="liip_functional_test.services_database_tools.ormdatabase_tool" />
</call>
<call method="add">
<argument type="service" id="liip_functional_test.services_database_tools.ormsqllite_database_tool" />
</call>
<call method="add">
<argument type="service" id="liip_functional_test.services_database_tools.mongo_dbdatabase_tool" />
</call>
<call method="add">
<argument type="service" id="liip_functional_test.services_database_tools.phpcrdatabase_tool" />
</call>
</service>
</services>
</container>
116 changes: 116 additions & 0 deletions src/Services/DatabaseBackup/AbstractDatabaseBackup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

/*
* This file is part of the Liip/FunctionalTestBundle
*
* (c) Lukas Kahwe Smith <[email protected]>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Liip\FunctionalTestBundle\Services\DatabaseBackup;

use Doctrine\Common\DataFixtures\Executor\AbstractExecutor;
use Doctrine\DBAL\Connection;
use Liip\FunctionalTestBundle\Services\FixturesLoaderFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* @author Aleksey Tupichenkov <[email protected]>
*/
abstract class AbstractDatabaseBackup
{
protected $container;

protected $fixturesLoaderFactory;

/**
* @var Connection
*/
protected $connection;

/**
* @var array
*/
protected $metadatas;

/**
* The fixture classnames.
*
* @var array
*/
protected $classNames;

public function __construct(ContainerInterface $container, FixturesLoaderFactory $fixturesLoaderFactory)
{
$this->container = $container;
$this->fixturesLoaderFactory = $fixturesLoaderFactory;
}

public function init(Connection $connection, array $metadatas, array $classNames): void
{
$this->connection = $connection;
$this->metadatas = $metadatas;
$this->classNames = $classNames;
}

abstract public function getBackupName(): string;

abstract public function isBackupActual(): bool;

abstract public function backup(AbstractExecutor $executor): void;

abstract public function restore(AbstractExecutor $executor): void;

/**
* Determine if the Fixtures that define a database backup have been
* modified since the backup was made.
*
* @param string $backup The fixture backup SQLite database file path
*
* @return bool TRUE if the backup was made since the modifications to the
* fixtures; FALSE otherwise
*/
protected function isBackupUpToDate(string $backup): bool
{
$backupLastModifiedDateTime = \DateTime::createFromFormat('U', filemtime($backup));

$loader = $this->fixturesLoaderFactory->getFixtureLoader($this->classNames);

// Use loader in order to fetch all the dependencies fixtures.
foreach ($loader->getFixtures() as $className) {
$fixtureLastModifiedDateTime = $this->getFixtureLastModified($className);
if ($backupLastModifiedDateTime < $fixtureLastModifiedDateTime) {
return false;
}
}

return true;
}

/**
* This function finds the time when the data blocks of a class definition
* file were being written to, that is, the time when the content of the
* file was changed.
*
* @param string $class The fully qualified class name of the fixture class to
* check modification date on
*
* @return \DateTime|null
*/
protected function getFixtureLastModified($class): ?\DateTime
{
$lastModifiedDateTime = null;

$reflClass = new \ReflectionClass($class);
$classFileName = $reflClass->getFileName();

if (file_exists($classFileName)) {
$lastModifiedDateTime = new \DateTime();
$lastModifiedDateTime->setTimestamp(filemtime($classFileName));
}

return $lastModifiedDateTime;
}
}
111 changes: 111 additions & 0 deletions src/Services/DatabaseBackup/MysqlCustomDatabaseBackup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

/*
* This file is part of the Liip/FunctionalTestBundle
*
* (c) Lukas Kahwe Smith <[email protected]>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Liip\FunctionalTestBundle\Services\DatabaseBackup;

use Doctrine\Common\DataFixtures\Executor\AbstractExecutor;
use Doctrine\ORM\EntityManager;

/**
* @author Aleksey Tupichenkov <[email protected]>
*
* It's class created just for example that how to create database backup/restore service
*/
class MysqlCustomDatabaseBackup extends AbstractDatabaseBackup
{
protected static $referenceData;

protected static $sql;

protected static $metadata;

public function getBackupName(): string
{
return $this->container->getParameter('kernel.cache_dir').'/test_mysql_'.md5(serialize($this->metadatas).serialize($this->classNames)).'.sql';
}

public function getReferenceBackupName(): string
{
return $this->getBackupName().'.ser';
}

protected function getBackup(): string
{
if (empty(self::$sql)) {
self::$sql = file_get_contents($this->getBackupName());
}

return self::$sql;
}

protected function getReferenceBackup(): string
{
if (empty(self::$referenceData)) {
self::$referenceData = file_get_contents($this->getReferenceBackupName());
}

return self::$referenceData;
}

public function isBackupActual(): bool
{
return
file_exists(file_exists($this->getBackupName())) &&
file_exists(file_exists($this->getReferenceBackupName())) &&
$this->isBackupUpToDate($this->getBackupName());
}

public function backup(AbstractExecutor $executor): void
{
$params = $this->connection->getParams();
if (isset($params['master'])) {
$params = $params['master'];
}

$dbName = isset($params['dbname']) ? $params['dbname'] : '';
$dbHost = isset($params['host']) ? $params['host'] : '';
$dbPort = isset($params['port']) ? $params['port'] : '';
$dbUser = isset($params['user']) ? $params['user'] : '';
$dbPass = isset($params['password']) ? $params['password'] : '';

$executor->getReferenceRepository()->save($this->getBackupName());
/** @var EntityManager $em */
$em = $executor->getReferenceRepository()->getManager();
self::$metadata = $em->getMetadataFactory()->getLoadedMetadata();

exec("mysqldump -h $dbHost -u $dbUser -p$dbPass --no-create-db $dbName > {$this->getBackupName()}");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexislefebvre @lsmith77
About #398 (comment), it's not suit, this method save only reference data, but I need save database data (sql with inserts data per each table)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed ..

Generally I think if we have to use a CLI command, then its not really a good fit for this Bundle, since it will not be applicable in too many cases.

This library here might help however https://github.com/portphp/doctrine

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... I added mysql class just for example
I can use portphp/doctrine or something else, it will be interesting for me, but I have fears about performance and additional vendor...

Currently I rename service mysql_custom -> mysql_example and add attention block from readme

As a proposal, mysql (and other) can be implemented by a separate repo in github, what do you think about it? @alexislefebvre @lsmith77

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alekseytupichenkov

About #398 (comment), it's not suit, this method save only reference data, but I need save database data (sql with inserts data per each table)

The method works on the current version of this bundle. I'm sorry but I don't understand why you don't use it in this PR. Could you please explain why you chose to perform a full export instead of using only the reference data?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexislefebvre I save reference too
https://github.com/liip/LiipFunctionalTestBundle/pull/398/files#diff-f2e540017ed6e7479f8d4a4b1e17a480R82

But it's not enough, I need backup both, DB and reference.
In case of SQLite, to copy DB enough copy just file, in case of MySql we can't just copy files, need make backup, for make backup I use mysqldump because Doctrine can't do it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lsmith77 about https://github.com/portphp/doctrine

This lib really cool, but still not suitable, this lib saves only entities, but I need dump full db.
For example, after loading fixtures I can have some additional data, by reason to Doctrine lifecycle methods or DB triggers

I found some PHP libs to make backup like https://github.com/backup-manager/backup-manager or https://github.com/spatie/db-dumper
But they all use mysqldump too

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your explanation, I was confused by the fact that SQLite and MySQL need different ways to perform backups. I understand now. Thanks.

}

public function restore(AbstractExecutor $executor): void
{
$this->connection->query('SET FOREIGN_KEY_CHECKS = 0;');
$tables = [];
foreach ($this->metadatas as $classMetadata) {
$tables[] = $classMetadata->table['name'];
}
$this->connection->query('DROP TABLE IF EXISTS '.implode(',', $tables));
$this->connection->query($this->getBackup());
$this->connection->query('SET FOREIGN_KEY_CHECKS = 1;');

/** @var EntityManager $em */
$em = $executor->getReferenceRepository()->getManager();
if (self::$metadata) {
// it need for better performance
foreach (self::$metadata as $class => $data) {
$em->getMetadataFactory()->setMetadataFor($class, $data);
}
$executor->getReferenceRepository()->unserialize($this->getReferenceBackup());
} else {
$executor->getReferenceRepository()->unserialize($this->getReferenceBackup());
self::$metadata = $em->getMetadataFactory()->getLoadedMetadata();
}
}
}
Loading