├── tests ├── Integration │ ├── database │ │ └── .gitkeep │ ├── Entity │ │ ├── Product │ │ │ └── TestProduct.php │ │ └── Category │ │ │ └── TestCategory.php │ ├── Dictionary │ │ ├── SqliteDictionary.php │ │ └── MySqlDictionary.php │ ├── PurgerTest.php │ ├── MySqlBackupTest.php │ ├── SqliteBackupTest.php │ ├── BasicPHPUnitUsageExampleTest.php │ ├── AdvancedPHPUnitUsageExampleTest.php │ └── IntegrationTestCase.php ├── bootstrap.php ├── Command │ ├── MysqldumpDummyCommand.php │ └── MysqldumpCommandTest.php ├── PurgerFactoryTest.php ├── Storage │ ├── InMemoryStorageTest.php │ └── LocalStorageTest.php ├── Backup │ ├── SqliteBackupTest.php │ └── MySqlBackupTest.php └── BackupFactoryTest.php ├── .gitignore ├── src ├── Command │ ├── Command.php │ └── MysqldumpCommand.php ├── PurgerFactory.php ├── Storage │ ├── Storage.php │ ├── LocalStorage.php │ └── InMemoryStorage.php ├── Backup │ ├── Backup.php │ ├── SqliteBackup.php │ └── MySqlBackup.php ├── DoctrineDatabaseBackup.php ├── BackupFactory.php └── Purger.php ├── .travis.yml ├── CHANGELOG.md ├── phpunit.xml.dist ├── composer.json └── README.md /tests/Integration/database/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .idea/ 3 | composer.lock 4 | composer.phar -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests 7 | ./tests/Integration 8 | 9 | 10 | 11 | ./tests/Integration 12 | 13 | 14 | 15 | 16 | 17 | ./ 18 | 19 | ./tests 20 | ./vendor 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lzakrzewski/doctrine-database-backup", 3 | "type": "library", 4 | "description": "Doctrine Database Backup", 5 | "require": { 6 | "php": ">=5.4", 7 | "doctrine/orm": "~2.3", 8 | "symfony/process": "~2.3|~3.0" 9 | }, 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "lzakrzewski", 14 | "email": "contact@lzakrzewski.com" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "Lzakrzewski\\DoctrineDatabaseBackup\\": "src" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-4" : { "Lzakrzewski\\DoctrineDatabaseBackup\\tests\\" : "tests" } 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit" : "~4.8", 27 | "mikey179/vfsStream": "~1", 28 | "satooshi/php-coveralls": "~1.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Storage/LocalStorage.php: -------------------------------------------------------------------------------- 1 | name = $name; 37 | $this->price = $price; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/PurgerFactoryTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('\Lzakrzewski\DoctrineDatabaseBackup\Purger', PurgerFactory::instance($this->entityManager->reveal())); 18 | } 19 | 20 | /** {@inheritdoc} */ 21 | protected function setUp() 22 | { 23 | $this->entityManager = $this->prophesize('\Doctrine\ORM\EntityManager'); 24 | } 25 | 26 | /** {@inheritdoc} */ 27 | protected function tearDown() 28 | { 29 | $this->entityManager = null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Integration/Dictionary/SqliteDictionary.php: -------------------------------------------------------------------------------- 1 | 'pdo_sqlite', 17 | 'user' => 'root', 18 | 'password' => '', 19 | 'path' => __DIR__.'/../database/sqlite.db', 20 | ]; 21 | } 22 | 23 | protected function setupDatabase() 24 | { 25 | $params = $this->getParams(); 26 | 27 | $tmpConnection = DriverManager::getConnection($params); 28 | $tmpConnection->getSchemaManager()->createDatabase($params['path']); 29 | 30 | $schemaTool = new SchemaTool($this->entityManager); 31 | $schemaTool->dropDatabase(); 32 | 33 | $productClass = $this->productClass(); 34 | $categoryClass = $this->categoryClass(); 35 | 36 | $schemaTool->createSchema([ 37 | $this->entityManager->getClassMetadata($productClass), 38 | $this->entityManager->getClassMetadata($categoryClass), 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Storage/InMemoryStorage.php: -------------------------------------------------------------------------------- 1 | registry[$key] = $value; 29 | } 30 | 31 | /** {@inheritdoc} */ 32 | public function read($key) 33 | { 34 | if (!$this->has($key)) { 35 | throw new \RuntimeException(sprintf('There is no object "%s" in memory', $key)); 36 | } 37 | 38 | return $this->registry[$key]; 39 | } 40 | 41 | /** {@inheritdoc} */ 42 | public function has($key) 43 | { 44 | return array_key_exists($key, $this->registry); 45 | } 46 | 47 | public function clear() 48 | { 49 | $this->registry = []; 50 | } 51 | 52 | private function __construct() 53 | { 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Integration/Entity/Category/TestCategory.php: -------------------------------------------------------------------------------- 1 | products = new ArrayCollection($products); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Integration/Dictionary/MySqlDictionary.php: -------------------------------------------------------------------------------- 1 | 'pdo_mysql', 17 | 'user' => 'root', 18 | 'password' => '', 19 | 'dbname' => 'doctrine-database-test', 20 | ]; 21 | } 22 | 23 | protected function setupDatabase() 24 | { 25 | $params = $this->getParams(); 26 | $name = $params['dbname']; 27 | 28 | unset($params['dbname']); 29 | 30 | $tmpConnection = DriverManager::getConnection($params); 31 | $nameEscaped = $tmpConnection->getDatabasePlatform()->quoteSingleIdentifier($name); 32 | 33 | if (in_array($name, $tmpConnection->getSchemaManager()->listDatabases())) { 34 | $tmpConnection->getSchemaManager()->dropDatabase($nameEscaped); 35 | } 36 | 37 | $tmpConnection->getSchemaManager()->createDatabase($nameEscaped); 38 | 39 | $productClass = $this->productClass(); 40 | $categoryClass = $this->categoryClass(); 41 | 42 | $schemaTool = new SchemaTool($this->entityManager); 43 | $schemaTool->createSchema([ 44 | $this->entityManager->getClassMetadata($productClass), 45 | $this->entityManager->getClassMetadata($categoryClass), 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/DoctrineDatabaseBackup.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 26 | 27 | $this->backup = BackupFactory::instance($entityManager); 28 | $this->purger = PurgerFactory::instance($entityManager); 29 | 30 | $this->purgeDB = $purgeDB; 31 | } 32 | 33 | public function restore(callable $setupDatabaseCallback = null) 34 | { 35 | if (!$this->backup->isBackupCreated()) { 36 | 37 | if ($this->purgeDB) { 38 | $this->purger->purge(); 39 | } 40 | 41 | if (null !== $setupDatabaseCallback) { 42 | $setupDatabaseCallback($this->entityManager); 43 | } 44 | 45 | $this->backup->create(); 46 | } 47 | 48 | $this->backup->restore(); 49 | } 50 | 51 | /** 52 | * @return Backup 53 | */ 54 | public function getBackup() 55 | { 56 | return $this->backup; 57 | } 58 | 59 | /** 60 | * @return Purger 61 | */ 62 | public function getPurger() 63 | { 64 | return $this->purger; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Integration/PurgerTest.php: -------------------------------------------------------------------------------- 1 | givenDatabaseContainsProducts(5); 20 | 21 | $this->purger->purge(); 22 | 23 | $this->assertThatDatabaseIsClear(); 24 | } 25 | 26 | /** @test */ 27 | public function it_purges_database_twice_with_cached_sql() 28 | { 29 | $this->givenDatabaseContainsProducts(5); 30 | 31 | $this->purger->purge(); 32 | 33 | $this->addProduct(); 34 | 35 | $this->purger->purge(); 36 | 37 | $this->assertThatDatabaseIsClear(); 38 | } 39 | 40 | /** @test */ 41 | public function it_purges_database_with_related_entities() 42 | { 43 | $this->givenDatabaseContainsProducts(5); 44 | $this->givenDatabaseContainsCategories(5); 45 | 46 | $this->purger->purge(); 47 | 48 | $this->assertThatDatabaseIsClear(); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | protected function setUp() 55 | { 56 | parent::setUp(); 57 | 58 | InMemoryStorage::instance()->clear(); 59 | 60 | $this->purger = new Purger($this->entityManager, InMemoryStorage::instance()); 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | protected function tearDown() 67 | { 68 | $this->purger = null; 69 | 70 | parent::tearDown(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Backup/SqliteBackup.php: -------------------------------------------------------------------------------- 1 | sourcePath = $sourcePath; 27 | $this->memoryStorage = $memoryStorage; 28 | $this->localStorage = $localStorage; 29 | } 30 | 31 | /** {@inheritdoc} */ 32 | public function create() 33 | { 34 | $sourcePath = $this->sourcePath; 35 | 36 | if (!$this->localStorage->has($sourcePath)) { 37 | throw new \RuntimeException(sprintf("Source database '%s' should exists.", $sourcePath)); 38 | } 39 | 40 | $this->memoryStorage->put(self::BACKUP_KEY, $this->localStorage->read($sourcePath)); 41 | } 42 | 43 | /** {@inheritdoc} */ 44 | public function restore() 45 | { 46 | if (!$this->isBackupCreated()) { 47 | throw new \RuntimeException('Backup file should be created before restore database.'); 48 | } 49 | 50 | $this->localStorage->put($this->sourcePath, $this->memoryStorage->read(self::BACKUP_KEY)); 51 | } 52 | 53 | /** {@inheritdoc} */ 54 | public function isBackupCreated() 55 | { 56 | return $this->memoryStorage->has(self::BACKUP_KEY); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Command/MysqldumpCommandTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 13 | "mysqldump 'dbname' --no-create-info --host='host' --port='3306' --user='user' --password='password'", 14 | $command->run() 15 | ); 16 | } 17 | 18 | /** @test */ 19 | public function it_can_call_command_to_dump_database_without_password() 20 | { 21 | $command = new MysqldumpDummyCommand('dbname', 'host', '3306', 'user'); 22 | 23 | $this->assertEquals( 24 | "mysqldump 'dbname' --no-create-info --host='host' --port='3306' --user='user'", 25 | $command->run() 26 | ); 27 | } 28 | 29 | /** @test */ 30 | public function it_can_call_command_to_dump_database_without_password_and_user() 31 | { 32 | $command = new MysqldumpDummyCommand('dbname', 'host', '3306'); 33 | 34 | $this->assertEquals( 35 | "mysqldump 'dbname' --no-create-info --host='host' --port='3306'", 36 | $command->run() 37 | ); 38 | } 39 | 40 | /** @test */ 41 | public function it_can_call_command_to_dump_database_without_password_and_user_and_port() 42 | { 43 | $command = new MysqldumpDummyCommand('dbname', 'host'); 44 | 45 | $this->assertEquals( 46 | "mysqldump 'dbname' --no-create-info --host='host'", 47 | $command->run() 48 | ); 49 | } 50 | 51 | /** @test */ 52 | public function it_can_call_command_to_dump_database_without_password_user_and_host_and_port() 53 | { 54 | $command = new MysqldumpDummyCommand('dbname'); 55 | 56 | $this->assertEquals( 57 | "mysqldump 'dbname' --no-create-info ", 58 | $command->run() 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Command/MysqldumpCommand.php: -------------------------------------------------------------------------------- 1 | dbname = $dbname; 30 | $this->host = $host; 31 | $this->port = $port; 32 | $this->user = $user; 33 | $this->password = $password; 34 | } 35 | 36 | /** {@inheritdoc} */ 37 | public function run() 38 | { 39 | $command = sprintf('mysqldump %s --no-create-info ', escapeshellarg($this->dbname)); 40 | 41 | if (null !== $this->host && strlen($this->host)) { 42 | $command .= sprintf(' --host=%s', escapeshellarg($this->host)); 43 | } 44 | 45 | if (null !== $this->port && strlen($this->port)) { 46 | $command .= sprintf(' --port=%s', escapeshellarg($this->port)); 47 | } 48 | 49 | if (null !== $this->user && strlen($this->user)) { 50 | $command .= sprintf(' --user=%s', escapeshellarg($this->user)); 51 | } 52 | 53 | if (null !== $this->password && strlen($this->password)) { 54 | $command .= sprintf(' --password=%s', escapeshellarg($this->password)); 55 | } 56 | 57 | return $this->execute($command); 58 | } 59 | 60 | protected function execute($command) 61 | { 62 | $process = new Process($command); 63 | $process->run(); 64 | 65 | if (!$process->isSuccessful()) { 66 | throw new \RuntimeException($process->getErrorOutput()); 67 | } 68 | 69 | return $process->getOutput(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Backup/MySqlBackup.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 32 | $this->memoryStorage = $memoryStorage; 33 | $this->purger = $purger; 34 | $this->command = $command; 35 | } 36 | 37 | /** {@inheritdoc} */ 38 | public function create() 39 | { 40 | $this->memoryStorage->put(self::BACKUP_KEY, $this->dataSql()); 41 | } 42 | 43 | /** {@inheritdoc} */ 44 | public function restore() 45 | { 46 | if (!$this->isBackupCreated()) { 47 | throw new \RuntimeException('Backup should be created before restore database.'); 48 | } 49 | 50 | $this->purger->purge(); 51 | 52 | if (null !== $dataSql = $this->memoryStorage->read(self::BACKUP_KEY)) { 53 | $this->execute($dataSql); 54 | } 55 | } 56 | 57 | /** {@inheritdoc} */ 58 | public function isBackupCreated() 59 | { 60 | return $this->memoryStorage->has(self::BACKUP_KEY); 61 | } 62 | 63 | private function execute($sql) 64 | { 65 | $this->connection->beginTransaction(); 66 | $this->connection->exec($sql); 67 | 68 | $this->connection->commit(); 69 | } 70 | 71 | private function dataSql() 72 | { 73 | $output = $this->command->run(); 74 | 75 | if (false === stripos($output, 'INSERT')) { 76 | return; 77 | } 78 | 79 | return $output; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Storage/InMemoryStorageTest.php: -------------------------------------------------------------------------------- 1 | assertSame(InMemoryStorage::instance(), $this->storage); 16 | } 17 | 18 | /** @test */ 19 | public function it_reads() 20 | { 21 | $value = new \stdClass(); 22 | 23 | $this->storage->put('test', $value); 24 | 25 | $this->assertSame($value, $this->storage->read('test')); 26 | } 27 | 28 | /** 29 | * @test 30 | * @expectedException \RuntimeException 31 | */ 32 | public function it_fails_when_try_to_read_not_existing_value() 33 | { 34 | $this->storage->read('test'); 35 | } 36 | 37 | /** @test */ 38 | public function it_has_value() 39 | { 40 | $value = new \stdClass(); 41 | 42 | $this->storage->put('test', $value); 43 | 44 | $this->assertTrue($this->storage->has('test')); 45 | } 46 | 47 | /** @test */ 48 | public function it_has_not_value() 49 | { 50 | $this->assertFalse($this->storage->has('test')); 51 | } 52 | 53 | /** @test */ 54 | public function it_has_null() 55 | { 56 | $this->storage->put('test', null); 57 | 58 | $this->assertTrue($this->storage->has('test')); 59 | } 60 | 61 | /** @test */ 62 | public function it_has_not_shared_value_by_default() 63 | { 64 | $this->assertFalse($this->storage->has('test')); 65 | } 66 | 67 | /** 68 | * @test 69 | * @expectedException \RuntimeException 70 | */ 71 | public function it_can_be_cleared() 72 | { 73 | $value = new \stdClass(); 74 | 75 | $this->storage->put('test', $value); 76 | $this->storage->clear(); 77 | 78 | $this->storage->read('test'); 79 | } 80 | 81 | /** {@inheritdoc} */ 82 | protected function setUp() 83 | { 84 | $this->storage = InMemoryStorage::instance(); 85 | $this->storage->clear(); 86 | } 87 | 88 | /** {@inheritdoc} */ 89 | protected function tearDown() 90 | { 91 | $this->storage = null; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/BackupFactory.php: -------------------------------------------------------------------------------- 1 | getConnection(); 19 | 20 | if ($connection->getDatabasePlatform() instanceof SqlitePlatform) { 21 | return self::sqliteBackup($entityManager); 22 | } 23 | 24 | if ($connection->getDatabasePlatform() instanceof MySqlPlatform) { 25 | return self::mySqlBackup($entityManager); 26 | } 27 | 28 | throw new \RuntimeException('Unsupported database platform. Currently "SqlitePlatform" is supported.'); 29 | } 30 | 31 | private static function sqliteBackup(EntityManager $entityManager) 32 | { 33 | $params = $entityManager->getConnection()->getParams(); 34 | 35 | if (false === isset($params['path']) || $params['path'] == ':memory:') { 36 | throw new \RuntimeException('Backup for Sqlite "in_memory" is not supported.'); 37 | } 38 | 39 | return new SqliteBackup($params['path'], InMemoryStorage::instance(), new LocalStorage()); 40 | } 41 | 42 | private static function mySqlBackup(EntityManager $entityManager) 43 | { 44 | $params = $entityManager->getConnection()->getParams(); 45 | 46 | if (false === isset($params['dbname'])) { 47 | throw new \RuntimeException('Database name should be provided'); 48 | } 49 | 50 | $host = (isset($params['host'])) ? $params['host'] : null; 51 | $port = (isset($params['port'])) ? $params['port'] : null; 52 | $user = (isset($params['user'])) ? $params['user'] : null; 53 | $password = (isset($params['password'])) ? $params['password'] : null; 54 | 55 | $purger = PurgerFactory::instance($entityManager); 56 | $command = new MysqldumpCommand($params['dbname'], $host, $port, $user, $password); 57 | 58 | return new MySqlBackup($entityManager->getConnection(), InMemoryStorage::instance(), $purger, $command); 59 | } 60 | 61 | private function __construct() 62 | { 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Storage/LocalStorageTest.php: -------------------------------------------------------------------------------- 1 | givenFileExists('vfs://project/source.db', 'contents'); 17 | 18 | $this->assertEquals('contents', $this->storage->read('vfs://project/source.db')); 19 | } 20 | 21 | /** 22 | * @test 23 | * 24 | * @expectedException \RuntimeException 25 | */ 26 | public function it_fails_during_reading_when_file_does_not_exists() 27 | { 28 | $this->storage->read('vfs://project/source.db'); 29 | } 30 | 31 | /** @test */ 32 | public function it_can_write_new_file() 33 | { 34 | $this->storage->put('vfs://project/source.db', 'contents'); 35 | 36 | $this->assertEquals('contents', $this->storage->read('vfs://project/source.db')); 37 | } 38 | 39 | /** @test */ 40 | public function it_can_write_existing_file() 41 | { 42 | $this->givenFileExists('vfs://project/source.db', 'old-contents'); 43 | 44 | $this->storage->put('vfs://project/source.db', 'contents'); 45 | 46 | $this->assertEquals('contents', $this->storage->read('vfs://project/source.db')); 47 | } 48 | 49 | /** 50 | * @test 51 | * 52 | * @expectedException \RuntimeException 53 | */ 54 | public function it_can_not_write_file_to_not_existing_directory() 55 | { 56 | $this->storage->put('vfs://project/not-existing/source.db', 'contents'); 57 | } 58 | 59 | /** @test */ 60 | public function it_checks_if_file_exists() 61 | { 62 | $this->givenFileExists('vfs://project/source.db'); 63 | 64 | $this->assertTrue($this->storage->has('vfs://project/source.db')); 65 | } 66 | 67 | /** @test */ 68 | public function it_checks_if_file_does_not_exists() 69 | { 70 | $this->assertFalse($this->storage->has('vfs://project/source.db')); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | protected function setUp() 77 | { 78 | vfsStream::setup('project'); 79 | 80 | $this->storage = new LocalStorage(); 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | protected function tearDown() 87 | { 88 | $this->storage = null; 89 | } 90 | 91 | private function givenFileExists($fileName, $contents = 'contents') 92 | { 93 | file_put_contents($fileName, $contents); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/Integration/MySqlBackupTest.php: -------------------------------------------------------------------------------- 1 | givenDatabaseIsClear(); 21 | 22 | $this->backup->getBackup()->create(); 23 | $this->addProduct(); 24 | 25 | $this->backup->restore(); 26 | 27 | $this->assertThatDatabaseIsClear(); 28 | } 29 | 30 | /** @test */ 31 | public function it_can_restore_database_with_data() 32 | { 33 | $this->givenDatabaseContainsProducts(5); 34 | 35 | $this->backup->getBackup()->create(); 36 | $this->addProduct(); 37 | 38 | $this->backup->getBackup()->restore(); 39 | 40 | $this->assertThatDatabaseContainProducts(5); 41 | } 42 | 43 | /** @test */ 44 | public function it_can_restore_database_with_callback() 45 | { 46 | $this->givenDatabaseIsClear(); 47 | 48 | $this->backup->restore(function (EntityManager $entityManager) { 49 | $product1 = $this->productInstance(); 50 | $product2 = $this->productInstance(); 51 | 52 | $entityManager->persist($product1); 53 | $entityManager->persist($product2); 54 | 55 | $entityManager->flush(); 56 | }); 57 | 58 | $this->assertThatDatabaseContainProducts(2); 59 | } 60 | 61 | /** @test */ 62 | public function it_can_clear_database() 63 | { 64 | $this->givenDatabaseContainsProducts(5); 65 | 66 | $this->backup->restore(); 67 | 68 | $this->assertThatDatabaseIsClear(); 69 | } 70 | 71 | /** @test */ 72 | public function it_confirms_that_backup_was_created() 73 | { 74 | $this->backup->getBackup()->create(); 75 | 76 | $this->assertTrue($this->backup->getBackup()->isBackupCreated()); 77 | } 78 | 79 | /** @test */ 80 | public function it_confirms_that_backup_was_not_created() 81 | { 82 | $this->assertFalse($this->backup->getBackup()->isBackupCreated()); 83 | } 84 | 85 | /** {@inheritdoc} */ 86 | protected function setUp() 87 | { 88 | parent::setUp(); 89 | 90 | $this->backup = new DoctrineDatabaseBackup($this->entityManager); 91 | 92 | $this->givenMemoryIsClear(); 93 | } 94 | 95 | /** {@inheritdoc} */ 96 | protected function tearDown() 97 | { 98 | $this->backup = null; 99 | 100 | parent::tearDown(); 101 | } 102 | 103 | private function givenMemoryIsClear() 104 | { 105 | InMemoryStorage::instance()->clear(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/Integration/SqliteBackupTest.php: -------------------------------------------------------------------------------- 1 | givenDatabaseIsClear(); 21 | 22 | $this->backup->getBackup()->create(); 23 | $this->addProduct(); 24 | 25 | $this->backup->restore(); 26 | 27 | $this->assertThatDatabaseIsClear(); 28 | } 29 | 30 | /** @test */ 31 | public function it_can_restore_database_with_data() 32 | { 33 | $this->givenDatabaseContainsProducts(5); 34 | 35 | $this->backup->getBackup()->create(); 36 | $this->addProduct(); 37 | 38 | $this->backup->getBackup()->restore(); 39 | 40 | $this->assertThatDatabaseContainProducts(5); 41 | } 42 | 43 | /** @test */ 44 | public function it_can_restore_database_with_callback() 45 | { 46 | $this->givenDatabaseIsClear(); 47 | 48 | $this->backup->restore(function (EntityManager $entityManager) { 49 | $product1 = $this->productInstance(); 50 | $product2 = $this->productInstance(); 51 | 52 | $entityManager->persist($product1); 53 | $entityManager->persist($product2); 54 | 55 | $entityManager->flush(); 56 | }); 57 | 58 | $this->assertThatDatabaseContainProducts(2); 59 | } 60 | 61 | /** @test */ 62 | public function it_can_clear_database() 63 | { 64 | $this->givenDatabaseContainsProducts(5); 65 | 66 | $this->backup->restore(); 67 | 68 | $this->assertThatDatabaseIsClear(); 69 | } 70 | 71 | /** @test */ 72 | public function it_confirms_that_backup_was_created() 73 | { 74 | $this->backup->getBackup()->create(); 75 | 76 | $this->assertTrue($this->backup->getBackup()->isBackupCreated()); 77 | } 78 | 79 | /** @test */ 80 | public function it_confirms_that_backup_was_not_created() 81 | { 82 | $this->assertFalse($this->backup->getBackup()->isBackupCreated()); 83 | } 84 | 85 | /** {@inheritdoc} */ 86 | protected function setUp() 87 | { 88 | parent::setUp(); 89 | 90 | $this->backup = new DoctrineDatabaseBackup($this->entityManager); 91 | 92 | $this->givenMemoryIsClear(); 93 | } 94 | 95 | /** {@inheritdoc} */ 96 | protected function tearDown() 97 | { 98 | $this->backup = null; 99 | 100 | parent::tearDown(); 101 | } 102 | 103 | private function givenMemoryIsClear() 104 | { 105 | InMemoryStorage::instance()->clear(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DoctrineDatabaseBackup 2 | 3 | [![Build Status](https://travis-ci.org/lzakrzewski/DoctrineDatabaseBackup.svg)](https://travis-ci.org/lzakrzewski/DoctrineDatabaseBackup) [![Latest Stable Version](https://poser.pugx.org/lucaszz/doctrine-database-backup/v/stable)](https://packagist.org/packages/lucaszz/doctrine-database-backup) [![Total Downloads](https://poser.pugx.org/lucaszz/doctrine-database-backup/downloads)](https://packagist.org/packages/lucaszz/doctrine-database-backup) [![Coverage Status](https://coveralls.io/repos/lzakrzewski/DoctrineDatabaseBackup/badge.svg?branch=master&service=github)](https://coveralls.io/github/lzakrzewski/DoctrineDatabaseBackup?branch=master) 4 | 5 | DoctrineDatabaseBackup is simple library for speed up tests in your app. 6 | It could be used for **PHPUnit** tests or **Behat** tests running from command line. 7 | My target was to avoid wasting time for dropping/creating or purging database for each test, so I optimized it. 8 | 9 | This library puts contents of database to memory and share it between every tests. 10 | 11 | **Notice:** I don't recommend to use this library with large fixtures because it can cause huge memory usage. 12 | I prefer to run tests with minimal database setup because it is more readable for me and it have better performance. 13 | 14 | Requirements 15 | ------------ 16 | ```json 17 | "require": { 18 | "php": ">=5.4", 19 | "doctrine/orm": "~2.3", 20 | "symfony/process": "~2.3" 21 | }, 22 | ``` 23 | 24 | Features 25 | -------- 26 | - It supports **SqlitePlatform** and **MySqlPlatform**, 27 | - It can create database backup per PHP process, 28 | - It can purge database in fast way, 29 | - It can restore database from backup before every test, 30 | - It can restore clear database before every test. 31 | 32 | Installation 33 | -------- 34 | Require the library with composer: 35 | 36 | ```sh 37 | composer require lzakrzewski/doctrine-database-backup "~1.2" 38 | ``` 39 | 40 | Basic usage (PHPUnit example) 41 | -------- 42 | ```php 43 | /** {@inheritdoc} */ 44 | protected function setUp() 45 | { 46 | parent::setUp(); 47 | 48 | $this->entityManager = $this->createEntityManager(); 49 | 50 | $backup = new DoctrineDatabaseBackup($this->entityManager); 51 | $backup->restore(); 52 | } 53 | ``` 54 | 55 | This database setup prepares clear database before every test. 56 | [Full working example](https://github.com/lzakrzewski/DoctrineDatabaseBackup/blob/master/tests/Integration/BasicPHPUnitUsageExampleTest.php). 57 | 58 | Advanced usage (PHPUnit example) 59 | -------- 60 | ```php 61 | /** {@inheritdoc} */ 62 | protected function setUp() 63 | { 64 | parent::setUp(); 65 | 66 | $this->entityManager = $this->createEntityManager(); 67 | $backup = new DoctrineDatabaseBackup($this->entityManager); 68 | 69 | $backup->restore(function (EntityManager $entityManager) { 70 | //your fixtures 71 | $entityManager->persist(new TestProduct('Iron', 99)); 72 | $entityManager->flush(); 73 | }); 74 | } 75 | ``` 76 | 77 | This database setup database with your fixtures before every test. 78 | [Full working example](https://github.com/lzakrzewski/DoctrineDatabaseBackup/blob/master/tests/Integration/AdvancedPHPUnitUsageExampleTest.php). 79 | 80 | **Notice:** that before first test of PHP process database should be created. 81 | 82 | Behat example 83 | -------- 84 | ```php 85 | /** @BeforeScenario*/ 86 | public function restoreDatabase() 87 | { 88 | // "getEntityManager" is your own getter for EntityManager 89 | $backup = new DoctrineDatabaseBackup($this->getEntityManager()); 90 | $backup->restore(); 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /tests/Backup/SqliteBackupTest.php: -------------------------------------------------------------------------------- 1 | givenDatabaseFileDoesNotExists(); 27 | 28 | $this->backup->create(); 29 | } 30 | 31 | /** @test */ 32 | public function it_creates_database_backup() 33 | { 34 | $this->givenDatabaseFileExists(); 35 | 36 | $this->backup->create(); 37 | 38 | $this->memoryStorage->put(SqliteBackup::BACKUP_KEY, 'contents')->shouldBeCalled(); 39 | } 40 | 41 | /** @test */ 42 | public function it_restores_database_from_backup() 43 | { 44 | $this->givenMemoryBackupExists(); 45 | 46 | $this->backup->restore(); 47 | 48 | $this->localStorage->put('/var/www/project/database/sqlite.db', 'contents')->shouldBeCalled(); 49 | } 50 | 51 | /** @test */ 52 | public function it_confirms_that_backup_was_created() 53 | { 54 | $this->givenMemoryBackupExists(); 55 | 56 | $this->assertTrue($this->backup->isBackupCreated()); 57 | } 58 | 59 | /** @test */ 60 | public function it_confirms_that_backup_was_not_created() 61 | { 62 | $this->givenMemoryBackupDoesNotExists(); 63 | 64 | $this->assertFalse($this->backup->isBackupCreated()); 65 | } 66 | 67 | /** {@inheritdoc} */ 68 | protected function setUp() 69 | { 70 | $this->memoryStorage = $this->prophesize('Lzakrzewski\DoctrineDatabaseBackup\Storage\InMemoryStorage'); 71 | $this->localStorage = $this->prophesize('Lzakrzewski\DoctrineDatabaseBackup\Storage\LocalStorage'); 72 | 73 | $this->backup = new SqliteBackup( 74 | '/var/www/project/database/sqlite.db', 75 | $this->memoryStorage->reveal(), 76 | $this->localStorage->reveal() 77 | ); 78 | } 79 | 80 | /** {@inheritdoc} */ 81 | protected function tearDown() 82 | { 83 | $this->memoryStorage = $this->prophesize('Lzakrzewski\DoctrineDatabaseBackup\Storage\InMemoryStorage'); 84 | $this->localStorage = $this->prophesize('Lzakrzewski\DoctrineDatabaseBackup\Storage\LocalStorage'); 85 | 86 | $this->backup = null; 87 | } 88 | 89 | private function givenDatabaseFileDoesNotExists() 90 | { 91 | $this->localStorage->has('/var/www/project/database/sqlite.db')->willReturn(false); 92 | } 93 | 94 | private function givenDatabaseFileExists() 95 | { 96 | $this->localStorage->has('/var/www/project/database/sqlite.db')->willReturn(true); 97 | $this->localStorage->read('/var/www/project/database/sqlite.db')->willReturn('contents'); 98 | } 99 | 100 | private function givenMemoryBackupExists() 101 | { 102 | $this->memoryStorage->has(SqliteBackup::BACKUP_KEY)->willReturn(true); 103 | $this->memoryStorage->read(SqliteBackup::BACKUP_KEY)->willReturn('contents'); 104 | } 105 | 106 | private function givenMemoryBackupDoesNotExists() 107 | { 108 | $this->memoryStorage->has(SqliteBackup::BACKUP_KEY)->willReturn(false); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/Integration/BasicPHPUnitUsageExampleTest.php: -------------------------------------------------------------------------------- 1 | entityManager->persist(new TestProduct('Teapot', 25)); 24 | $this->entityManager->flush(); 25 | 26 | $this->assertCount(1, $this->entityManager->getRepository('\Lzakrzewski\DoctrineDatabaseBackup\tests\Integration\Entity\Product\TestProduct')->findAll()); 27 | } 28 | 29 | public function testThatDatabaseIsClear() 30 | { 31 | $this->assertCount(0, $this->entityManager->getRepository('\Lzakrzewski\DoctrineDatabaseBackup\tests\Integration\Entity\Product\TestProduct')->findAll()); 32 | } 33 | 34 | /** 35 | * Before first test of PHP process database should be created. 36 | */ 37 | public static function setUpBeforeClass() 38 | { 39 | $entityManager = self::createEntityManager(); 40 | self::setupDatabase($entityManager); 41 | 42 | //Should be called only if another test in current PHP process created backup. 43 | InMemoryStorage::instance()->clear(); 44 | } 45 | 46 | /** {@inheritdoc} */ 47 | protected function setUp() 48 | { 49 | parent::setUp(); 50 | 51 | $this->entityManager = $this->createEntityManager(); 52 | 53 | $backup = new DoctrineDatabaseBackup($this->entityManager); 54 | $backup->restore(); 55 | } 56 | 57 | /** {@inheritdoc} */ 58 | protected function tearDown() 59 | { 60 | $this->entityManager = null; 61 | } 62 | 63 | /** 64 | * Example of creating EntityManager. 65 | */ 66 | private static function createEntityManager() 67 | { 68 | $entityPath = [__DIR__.'/Entity/Product']; 69 | 70 | $config = Setup::createAnnotationMetadataConfiguration($entityPath, false); 71 | $driver = new AnnotationDriver(new AnnotationReader(), $entityPath); 72 | AnnotationRegistry::registerLoader('class_exists'); 73 | $config->setMetadataDriverImpl($driver); 74 | 75 | return EntityManager::create(self::getParams(), $config); 76 | } 77 | 78 | /** 79 | * Example of setup database before test. 80 | */ 81 | private static function setupDatabase(EntityManager $entityManager) 82 | { 83 | $params = self::getParams(); 84 | 85 | $tmpConnection = DriverManager::getConnection($params); 86 | $tmpConnection->getSchemaManager()->createDatabase($params['path']); 87 | 88 | $schemaTool = new SchemaTool($entityManager); 89 | $schemaTool->dropDatabase(); 90 | 91 | $class = 'Lzakrzewski\DoctrineDatabaseBackup\tests\Integration\Entity\Product\TestProduct'; 92 | 93 | $schemaTool->createSchema([$entityManager->getClassMetadata($class)]); 94 | } 95 | 96 | /** 97 | * Example of Database connection parameters. 98 | */ 99 | private static function getParams() 100 | { 101 | return [ 102 | 'driver' => 'pdo_sqlite', 103 | 'user' => 'root', 104 | 'password' => '', 105 | 'path' => __DIR__.'/database/sqlite.db', 106 | ]; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/Integration/AdvancedPHPUnitUsageExampleTest.php: -------------------------------------------------------------------------------- 1 | entityManager->persist(new TestProduct('Teapot', 25)); 24 | $this->entityManager->flush(); 25 | 26 | $this->assertCount(2, $this->entityManager->getRepository('\Lzakrzewski\DoctrineDatabaseBackup\tests\Integration\Entity\Product\TestProduct')->findAll()); 27 | } 28 | 29 | public function testThatDatabaseContainsFixtures() 30 | { 31 | $this->assertCount(1, $this->entityManager->getRepository('\Lzakrzewski\DoctrineDatabaseBackup\tests\Integration\Entity\Product\TestProduct')->findAll()); 32 | } 33 | 34 | /** 35 | * Before first test of PHP process database should be created. 36 | */ 37 | public static function setUpBeforeClass() 38 | { 39 | $entityManager = self::createEntityManager(); 40 | self::setupDatabase($entityManager); 41 | 42 | //Should be called only if another test in current PHP process created backup. 43 | InMemoryStorage::instance()->clear(); 44 | } 45 | 46 | /** {@inheritdoc} */ 47 | protected function setUp() 48 | { 49 | parent::setUp(); 50 | 51 | $this->entityManager = $this->createEntityManager(); 52 | $backup = new DoctrineDatabaseBackup($this->entityManager); 53 | 54 | $backup->restore(function (EntityManager $entityManager) { 55 | //your fixtures 56 | $entityManager->persist(new TestProduct('Iron', 99)); 57 | $entityManager->flush(); 58 | }); 59 | } 60 | 61 | /** {@inheritdoc} */ 62 | protected function tearDown() 63 | { 64 | $this->entityManager = null; 65 | } 66 | 67 | /** 68 | * Example of creating EntityManager. 69 | */ 70 | private static function createEntityManager() 71 | { 72 | $entityPath = [__DIR__.'/Entity/Product']; 73 | 74 | $config = Setup::createAnnotationMetadataConfiguration($entityPath, false); 75 | $driver = new AnnotationDriver(new AnnotationReader(), $entityPath); 76 | AnnotationRegistry::registerLoader('class_exists'); 77 | $config->setMetadataDriverImpl($driver); 78 | 79 | return EntityManager::create(self::getParams(), $config); 80 | } 81 | 82 | /** 83 | * Example of setup database before test. 84 | */ 85 | private static function setupDatabase(EntityManager $entityManager) 86 | { 87 | $params = self::getParams(); 88 | 89 | $tmpConnection = DriverManager::getConnection($params); 90 | $tmpConnection->getSchemaManager()->createDatabase($params['path']); 91 | 92 | $schemaTool = new SchemaTool($entityManager); 93 | $schemaTool->dropDatabase(); 94 | 95 | $class = 'Lzakrzewski\DoctrineDatabaseBackup\tests\Integration\Entity\Product\TestProduct'; 96 | 97 | $schemaTool->createSchema([$entityManager->getClassMetadata($class)]); 98 | } 99 | 100 | /** 101 | * Example of Database connection parameters. 102 | */ 103 | private static function getParams() 104 | { 105 | return [ 106 | 'driver' => 'pdo_sqlite', 107 | 'user' => 'root', 108 | 'password' => '', 109 | 'path' => __DIR__.'/database/sqlite.db', 110 | ]; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/Integration/IntegrationTestCase.php: -------------------------------------------------------------------------------- 1 | entityManager = $this->createEntityManager(); 29 | 30 | $this->setupDatabase(); 31 | 32 | $this->products = $this->entityManager->getRepository($this->productClass()); 33 | $this->categories = $this->entityManager->getRepository($this->categoryClass()); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | protected function tearDown() 40 | { 41 | $this->entityManager = null; 42 | $this->products = null; 43 | $this->categories = null; 44 | } 45 | 46 | /** 47 | * @return EntityManager 48 | */ 49 | private function createEntityManager() 50 | { 51 | $entityPath = [__DIR__.'/Entity']; 52 | 53 | $config = Setup::createAnnotationMetadataConfiguration($entityPath, false); 54 | $driver = new AnnotationDriver(new AnnotationReader(), $entityPath); 55 | AnnotationRegistry::registerLoader('class_exists'); 56 | $config->setMetadataDriverImpl($driver); 57 | 58 | return EntityManager::create($this->getParams(), $config); 59 | } 60 | 61 | /** 62 | * @return TestProduct 63 | */ 64 | protected function productInstance() 65 | { 66 | return new TestProduct(uniqid(), rand(1, 1000) / 3); 67 | } 68 | 69 | /** 70 | * @return TestCategory 71 | */ 72 | protected function categoryInstance() 73 | { 74 | return new TestCategory([]); 75 | } 76 | 77 | /** 78 | * @return string 79 | */ 80 | protected function productClass() 81 | { 82 | return get_class($this->productInstance()); 83 | } 84 | 85 | /** 86 | * @return string 87 | */ 88 | protected function categoryClass() 89 | { 90 | return get_class($this->categoryInstance()); 91 | } 92 | 93 | protected function addProduct() 94 | { 95 | $this->entityManager->persist($this->productInstance()); 96 | $this->entityManager->flush(); 97 | } 98 | 99 | protected function givenDatabaseIsClear() 100 | { 101 | } 102 | 103 | protected function givenDatabaseContainsProducts($productsCount) 104 | { 105 | $this->givenDatabaseIsClear(); 106 | 107 | for ($productCount = 1; $productCount <= $productsCount; ++$productCount) { 108 | $this->addProduct(); 109 | } 110 | } 111 | 112 | protected function givenDatabaseContainsCategories($categoriesCount) 113 | { 114 | for ($categoryCount = 1; $categoryCount <= $categoriesCount; ++$categoryCount) { 115 | $this->entityManager->persist(new TestCategory($this->products->findAll())); 116 | $this->entityManager->flush(); 117 | } 118 | } 119 | 120 | protected function assertThatDatabaseContainProducts($expectedProductsCount) 121 | { 122 | $this->assertCount($expectedProductsCount, $this->products->findAll()); 123 | } 124 | 125 | protected function assertThatDatabaseIsClear() 126 | { 127 | $this->assertEmpty($this->products->findAll()); 128 | $this->assertEmpty($this->categories->findAll()); 129 | } 130 | 131 | /** 132 | * @return array 133 | */ 134 | abstract protected function getParams(); 135 | 136 | abstract protected function setupDatabase(); 137 | } 138 | -------------------------------------------------------------------------------- /tests/BackupFactoryTest.php: -------------------------------------------------------------------------------- 1 | givenSqliteDatabasePlatformWasEnabled(); 21 | 22 | $backup = BackupFactory::instance($this->entityManager->reveal()); 23 | 24 | $this->assertInstanceOf('\Lzakrzewski\DoctrineDatabaseBackup\Backup\SqliteBackup', $backup); 25 | } 26 | 27 | /** @test */ 28 | public function it_can_create_instance_of_mysql_backup() 29 | { 30 | $this->givenMySqlDatabasePlatformWasEnabled(); 31 | 32 | $backup = BackupFactory::instance($this->entityManager->reveal()); 33 | 34 | $this->assertInstanceOf('\Lzakrzewski\DoctrineDatabaseBackup\Backup\MySqlBackup', $backup); 35 | } 36 | 37 | /** 38 | * @test 39 | * @expectedException \RuntimeException 40 | */ 41 | public function it_can_not_create_instance_of_backup_with_unsupported_platform() 42 | { 43 | $this->givenUnsupportedDatabasePlatformWasEnabled(); 44 | 45 | BackupFactory::instance($this->entityManager->reveal()); 46 | } 47 | 48 | /** 49 | * @test 50 | * @expectedException \RuntimeException 51 | */ 52 | public function it_can_not_instance_of_sqlite_in_memory_backup() 53 | { 54 | $this->givenSqliteInMemoryDatabasePlatformWasEnabled(); 55 | 56 | BackupFactory::instance($this->entityManager->reveal()); 57 | } 58 | 59 | /** 60 | * @test 61 | * @expectedException \RuntimeException 62 | */ 63 | public function it_can_not_create_backup_with_mysql_platform_without_dbname_provided() 64 | { 65 | $this->givenMySqlWithoutDbnameDatabasePlatformWasEnabled(); 66 | 67 | BackupFactory::instance($this->entityManager->reveal()); 68 | } 69 | 70 | /** {@inheritdoc} */ 71 | protected function setUp() 72 | { 73 | $this->entityManager = $this->prophesize('\Doctrine\ORM\EntityManager'); 74 | $this->connection = $this->prophesize('\Doctrine\DBAL\Connection'); 75 | 76 | $this->entityManager->getConnection()->willReturn($this->connection->reveal()); 77 | } 78 | 79 | /** {@inheritdoc} */ 80 | protected function tearDown() 81 | { 82 | $this->entityManager = null; 83 | $this->connection = null; 84 | } 85 | 86 | private function givenSqliteDatabasePlatformWasEnabled() 87 | { 88 | $sqlitePlatform = $this->prophesize('\Doctrine\DBAL\Platforms\SqlitePlatform'); 89 | 90 | $this->connection->getDatabasePlatform()->willReturn($sqlitePlatform->reveal()); 91 | $this->connection->getParams()->willReturn(['path' => '/path/to/database.db']); 92 | } 93 | 94 | private function givenMySqlDatabasePlatformWasEnabled() 95 | { 96 | $mySqlPlatform = $this->prophesize('\Doctrine\DBAL\Platforms\MySqlPlatform'); 97 | 98 | $this->connection->getDatabasePlatform()->willReturn($mySqlPlatform->reveal()); 99 | $this->connection->getParams()->willReturn(['dbname' => 'test', 'host' => 'localhost', 'port' => '3306', 'user' => 'johndoe', 'password' => 'testing1']); 100 | } 101 | 102 | private function givenUnsupportedDatabasePlatformWasEnabled() 103 | { 104 | $unsupportedPlatform = $this->prophesize('\Doctrine\DBAL\Platforms\OraclePlatform'); 105 | 106 | $this->connection->getDatabasePlatform()->willReturn($unsupportedPlatform->reveal()); 107 | } 108 | 109 | private function givenSqliteInMemoryDatabasePlatformWasEnabled() 110 | { 111 | $this->givenSqliteDatabasePlatformWasEnabled(); 112 | 113 | $this->connection->getParams()->willReturn(['path' => ':memory:']); 114 | } 115 | 116 | private function givenMySqlWithoutDbnameDatabasePlatformWasEnabled() 117 | { 118 | $this->givenMySqlDatabasePlatformWasEnabled(); 119 | 120 | $this->connection->getParams()->willReturn(['host' => 'localhost']); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/Backup/MySqlBackupTest.php: -------------------------------------------------------------------------------- 1 | command->run()->willReturn(null); 29 | 30 | $this->backup->create(); 31 | 32 | $this->memoryStorage->put(MySqlBackup::BACKUP_KEY, null)->shouldBeCalled(); 33 | } 34 | 35 | /** @test */ 36 | public function it_creates_backup_with_data() 37 | { 38 | $this->command->run()->willReturn('INSERT INTO table VALUES (1, 2, 3, 4)'); 39 | 40 | $this->backup->create(); 41 | 42 | $this->memoryStorage->put(MySqlBackup::BACKUP_KEY, 'INSERT INTO table VALUES (1, 2, 3, 4)')->shouldBeCalled(); 43 | } 44 | 45 | /** @test */ 46 | public function it_restores_database() 47 | { 48 | $this->givenMemoryBackupExists(); 49 | 50 | $this->backup->restore(); 51 | 52 | $this->purger->purge()->shouldBeCalled(); 53 | $this->memoryStorage->read(MySqlBackup::BACKUP_KEY)->shouldBeCalled(); 54 | } 55 | 56 | /** @test */ 57 | public function it_restores_database_with_data() 58 | { 59 | $this->givenMemoryBackupWithDataExists(); 60 | 61 | $this->backup->restore(); 62 | 63 | $this->purger->purge()->shouldBeCalled(); 64 | $this->connection->beginTransaction()->shouldBeCalled(); 65 | $this->connection->exec('INSERT INTO table VALUES (1, 2, 3, 4)')->shouldBeCalled(); 66 | $this->connection->commit()->shouldBeCalled(); 67 | } 68 | 69 | /** @test */ 70 | public function it_confirms_that_backup_was_created() 71 | { 72 | $this->givenMemoryBackupExists(); 73 | 74 | $this->assertTrue($this->backup->isBackupCreated()); 75 | } 76 | 77 | /** @test */ 78 | public function it_confirms_that_backup_was_not_created() 79 | { 80 | $this->givenMemoryBackupDoesNotExists(); 81 | 82 | $this->assertFalse($this->backup->isBackupCreated()); 83 | } 84 | 85 | /** 86 | * @test 87 | * @expectedException \RuntimeException 88 | */ 89 | public function it_fails_during_restoring_database_without_backup() 90 | { 91 | $this->backup->restore(); 92 | } 93 | 94 | /** {@inheritdoc} */ 95 | protected function setUp() 96 | { 97 | $this->connection = $this->prophesize('\Doctrine\DBAL\Connection'); 98 | $this->memoryStorage = $this->prophesize('\Lzakrzewski\DoctrineDatabaseBackup\Storage\InMemoryStorage'); 99 | $this->purger = $this->prophesize('\Lzakrzewski\DoctrineDatabaseBackup\Purger'); 100 | $this->command = $this->prophesize('\Lzakrzewski\DoctrineDatabaseBackup\Command\Command'); 101 | 102 | $this->backup = new MySqlBackup($this->connection->reveal(), $this->memoryStorage->reveal(), $this->purger->reveal(), $this->command->reveal()); 103 | } 104 | 105 | /** {@inheritdoc} */ 106 | protected function tearDown() 107 | { 108 | $this->connection = null; 109 | $this->memoryStorage = null; 110 | $this->purger = null; 111 | $this->command = null; 112 | 113 | $this->backup = null; 114 | } 115 | 116 | private function givenMemoryBackupExists() 117 | { 118 | $this->memoryStorage->has(MySqlBackup::BACKUP_KEY)->willReturn(true); 119 | $this->memoryStorage->read(MySqlBackup::BACKUP_KEY)->willReturn('contents'); 120 | } 121 | 122 | private function givenMemoryBackupDoesNotExists() 123 | { 124 | $this->memoryStorage->has(MySqlBackup::BACKUP_KEY)->willReturn(false); 125 | } 126 | 127 | private function givenMemoryBackupWithDataExists() 128 | { 129 | $this->memoryStorage->has(MySqlBackup::BACKUP_KEY)->willReturn(true); 130 | $this->memoryStorage->read(MySqlBackup::BACKUP_KEY)->willReturn('INSERT INTO table VALUES (1, 2, 3, 4)'); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Purger.php: -------------------------------------------------------------------------------- 1 | 10 | * and Jonathan H. Wage , Benjamin Eberlei 11 | */ 12 | 13 | namespace Lzakrzewski\DoctrineDatabaseBackup; 14 | 15 | use Doctrine\DBAL\Platforms\AbstractPlatform; 16 | use Doctrine\ORM\EntityManager; 17 | use Doctrine\ORM\Internal\CommitOrderCalculator; 18 | use Doctrine\ORM\Mapping\ClassMetadata; 19 | use Lzakrzewski\DoctrineDatabaseBackup\Storage\InMemoryStorage; 20 | 21 | class Purger 22 | { 23 | const PURGER_KEY = 'purger'; 24 | 25 | /** @var EntityManager */ 26 | private $entityManager; 27 | /** @var InMemoryStorage */ 28 | private $memoryStorage; 29 | 30 | /** 31 | * @param EntityManager $entityManager 32 | * @param InMemoryStorage $memoryStorage 33 | */ 34 | public function __construct(EntityManager $entityManager, InMemoryStorage $memoryStorage) 35 | { 36 | $this->entityManager = $entityManager; 37 | $this->memoryStorage = $memoryStorage; 38 | } 39 | 40 | public function purge() 41 | { 42 | if (false === $this->memoryStorage->has(self::PURGER_KEY)) { 43 | $this->memoryStorage->put(self::PURGER_KEY, $this->purgeSql()); 44 | } 45 | 46 | $this->execute($this->memoryStorage->read(self::PURGER_KEY)); 47 | } 48 | 49 | private function execute($sql) 50 | { 51 | $connection = $this->entityManager->getConnection(); 52 | $connection->beginTransaction(); 53 | 54 | $connection->exec($sql); 55 | 56 | $connection->commit(); 57 | } 58 | 59 | private function purgeSql() 60 | { 61 | $sql = ''; 62 | $classes = []; 63 | $metadatas = $this->entityManager->getMetadataFactory()->getAllMetadata(); 64 | 65 | foreach ($metadatas as $metadata) { 66 | if (!$metadata->isMappedSuperclass && !(isset($metadata->isEmbeddedClass) && $metadata->isEmbeddedClass)) { 67 | $classes[] = $metadata; 68 | } 69 | } 70 | 71 | $commitOrder = $this->getCommitOrder($classes); 72 | 73 | // Get platform parameters 74 | $platform = $this->entityManager->getConnection()->getDatabasePlatform(); 75 | 76 | // Drop association tables first 77 | $orderedTables = $this->getAssociationTables($commitOrder, $platform); 78 | 79 | // Drop tables in reverse commit order 80 | for ($i = count($commitOrder) - 1; $i >= 0; --$i) { 81 | $class = $commitOrder[$i]; 82 | 83 | if (($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName) || 84 | (isset($class->isEmbeddedClass) && $class->isEmbeddedClass) || 85 | $class->isMappedSuperclass 86 | ) { 87 | continue; 88 | } 89 | 90 | $orderedTables[] = $this->getTableName($class, $platform); 91 | } 92 | 93 | foreach ($orderedTables as $tbl) { 94 | $sql .= sprintf('DELETE FROM %s;', $tbl); 95 | } 96 | 97 | return $sql; 98 | } 99 | 100 | private function getCommitOrder(array $classes) 101 | { 102 | $calc = new CommitOrderCalculator(); 103 | 104 | foreach ($classes as $class) { 105 | $calc->addClass($class); 106 | 107 | // $class before its parents 108 | foreach ($class->parentClasses as $parentClass) { 109 | $parentClass = $this->entityManager->getClassMetadata($parentClass); 110 | 111 | if (!$calc->hasClass($parentClass->name)) { 112 | $calc->addClass($parentClass); 113 | } 114 | 115 | $calc->addDependency($class, $parentClass); 116 | } 117 | 118 | foreach ($class->associationMappings as $assoc) { 119 | if ($assoc['isOwningSide']) { 120 | $targetClass = $this->entityManager->getClassMetadata($assoc['targetEntity']); 121 | 122 | if (!$calc->hasClass($targetClass->name)) { 123 | $calc->addClass($targetClass); 124 | } 125 | 126 | // add dependency ($targetClass before $class) 127 | $calc->addDependency($targetClass, $class); 128 | 129 | // parents of $targetClass before $class, too 130 | foreach ($targetClass->parentClasses as $parentClass) { 131 | $parentClass = $this->entityManager->getClassMetadata($parentClass); 132 | 133 | if (!$calc->hasClass($parentClass->name)) { 134 | $calc->addClass($parentClass); 135 | } 136 | 137 | $calc->addDependency($parentClass, $class); 138 | } 139 | } 140 | } 141 | } 142 | 143 | return $calc->getCommitOrder(); 144 | } 145 | 146 | private function getAssociationTables(array $classes, AbstractPlatform $platform) 147 | { 148 | $associationTables = []; 149 | 150 | foreach ($classes as $class) { 151 | foreach ($class->associationMappings as $assoc) { 152 | if ($assoc['isOwningSide'] && $assoc['type'] == ClassMetadata::MANY_TO_MANY) { 153 | $associationTables[] = $this->getJoinTableName($assoc, $class, $platform); 154 | } 155 | } 156 | } 157 | 158 | return $associationTables; 159 | } 160 | 161 | private function getTableName($class, $platform) 162 | { 163 | if (isset($class->table['schema']) && !method_exists($class, 'getSchemaName')) { 164 | return $class->table['schema'].'.'.$this->entityManager->getConfiguration()->getQuoteStrategy()->getTableName($class, $platform); 165 | } 166 | 167 | return $this->entityManager->getConfiguration()->getQuoteStrategy()->getTableName($class, $platform); 168 | } 169 | 170 | private function getJoinTableName($assoc, $class, $platform) 171 | { 172 | if (isset($assoc['joinTable']['schema']) && !method_exists($class, 'getSchemaName')) { 173 | return $assoc['joinTable']['schema'].'.'.$this->entityManager->getConfiguration()->getQuoteStrategy()->getJoinTableName($assoc, $class, $platform); 174 | } 175 | 176 | return $this->entityManager->getConfiguration()->getQuoteStrategy()->getJoinTableName($assoc, $class, $platform); 177 | } 178 | } 179 | --------------------------------------------------------------------------------