diff --git a/composer.json b/composer.json index 0d2c59a..984f790 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,10 @@ "nette/utils": "~2.3", "nextras/dbal": "~1.0 | ~2.0", "mockery/mockery": "~0.9", + "symfony/config": "~2.6 | ~3.0", "symfony/console": "~2.6 | ~3.0", + "symfony/dependency-injection": "~2.6 | ~3.0", + "symfony/http-kernel": "~2.6 | ~3.0", "ext-openssl": "*" }, "suggest": { diff --git a/doc/default.texy b/doc/default.texy index cb3892c..16c863f 100644 --- a/doc/default.texy +++ b/doc/default.texy @@ -91,6 +91,7 @@ Open the script in your browser (`HttpController`) or in a terminal (`ConsoleCon Organizing Migrations ===================== +Migrations are executed in alphabetical order (by filename), e.g. `structures/2015-03-17-aaa.sql` is executed before `dummy-data/2015-03-17-zzz.sql`. The following structure is recommended and used by Symfony Console Commands by default: /-- diff --git a/src/Bridges/Dibi/Dibi2Adapter.php b/src/Bridges/Dibi/Dibi2Adapter.php index 5d2e2ea..ffccb06 100644 --- a/src/Bridges/Dibi/Dibi2Adapter.php +++ b/src/Bridges/Dibi/Dibi2Adapter.php @@ -49,7 +49,7 @@ public function escapeString($value) public function escapeInt($value) { - return (int) $value; + return (string) (int) $value; } diff --git a/src/Bridges/Dibi/Dibi3Adapter.php b/src/Bridges/Dibi/Dibi3Adapter.php index 1b043d4..350a081 100644 --- a/src/Bridges/Dibi/Dibi3Adapter.php +++ b/src/Bridges/Dibi/Dibi3Adapter.php @@ -48,7 +48,7 @@ public function escapeString($value) public function escapeInt($value) { - return (int) $value; + return (string) (int) $value; } diff --git a/src/Bridges/NetteDI/IMigrationGroupsProvider.php b/src/Bridges/NetteDI/IMigrationGroupsProvider.php new file mode 100644 index 0000000..6bc911c --- /dev/null +++ b/src/Bridges/NetteDI/IMigrationGroupsProvider.php @@ -0,0 +1,22 @@ + NULL, 'phpParams' => [], 'driver' => NULL, 'dbal' => NULL, + 'groups' => NULL, // null|array 'diffGenerator' => TRUE, // false|doctrine 'withDummyData' => FALSE, - 'contentSource' => NULL, // CreateCommand::CONTENT_SOURCE_* 'ignoredQueriesFile' => NULL, ]; @@ -44,48 +47,44 @@ class MigrationsExtension extends Nette\DI\CompilerExtension 'pgsql' => 'Nextras\Migrations\Drivers\PgSqlDriver', ]; - /** - * Processes configuration data. Intended to be overridden by descendant. - * @return void - */ public function loadConfiguration() { - $builder = $this->getContainerBuilder(); $config = $this->validateConfig($this->defaults); - Validators::assertField($config, 'dir', 'string|Nette\PhpGenerator\PhpLiteral'); - Validators::assertField($config, 'phpParams', 'array'); - Validators::assertField($config, 'contentSource', 'string|null'); - Validators::assertField($config, 'ignoredQueriesFile', 'string|null'); - $dbal = $this->getDbal($config['dbal']); - $driver = $this->getDriver($config['driver'], $dbal); + // dbal + Validators::assertField($config, 'dbal', 'null|string|Nette\DI\Statement'); + $dbal = $this->getDbalDefinition($config['dbal']); - $configuration = $builder->addDefinition($this->prefix('configuration')) - ->setClass('Nextras\Migrations\Configurations\DefaultConfiguration') - ->setArguments([$config['dir'], $driver, $config['withDummyData'], $config['phpParams']]); + // driver + Validators::assertField($config, 'driver', 'null|string|Nette\DI\Statement'); + $driver = $this->getDriverDefinition($config['driver'], $dbal); - $builder->addExcludedClasses(['Nextras\Migrations\Bridges\SymfonyConsole\BaseCommand']); - $builder->addDefinition($this->prefix('continueCommand')) - ->setClass('Nextras\Migrations\Bridges\SymfonyConsole\ContinueCommand') - ->setArguments([$driver, $configuration]) - ->addTag('kdyby.console.command'); - $builder->addDefinition($this->prefix('createCommand')) - ->setClass('Nextras\Migrations\Bridges\SymfonyConsole\CreateCommand') - ->setArguments([$driver, $configuration]) - ->addTag('kdyby.console.command'); - $builder->addDefinition($this->prefix('resetCommand')) - ->setClass('Nextras\Migrations\Bridges\SymfonyConsole\ResetCommand') - ->setArguments([$driver, $configuration]) - ->addTag('kdyby.console.command'); + // diffGenerator + if ($config['diffGenerator'] === 'doctrine') { + Validators::assertField($config, 'ignoredQueriesFile', 'null|string'); + $this->createDoctrineStructureDiffGeneratorDefinition($config['ignoredQueriesFile']); + } + + // groups + if ($config['groups'] === NULL) { + Validators::assertField($config, 'dir', 'string|Nette\PhpGenerator\PhpLiteral'); + Validators::assertField($config, 'withDummyData', 'bool'); + $config['groups'] = $this->createDefaultGroupConfiguration($config['dir'], $config['withDummyData']); + } - if ($config['diffGenerator'] !== FALSE) { - $builder->addDefinition($this->prefix('structureDiffGenerator')) - ->setClass('Nextras\Migrations\IDiffGenerator') - ->setDynamic(); // hack to suppress "Class Nextras\Migrations\IDiffGenerator (...) not found" + Validators::assertField($config, 'groups', 'array'); + $groups = $this->createGroupDefinitions($config['groups']); - if ($config['diffGenerator'] === 'doctrine') { - $this->configureDoctrineStructureDiffGenerator(); - } + // extensionHandlers + Validators::assertField($config, 'phpParams', 'array'); + $extensionHandlers = $this->createExtensionHandlerDefinitions($driver, $config['phpParams']); + + // configuration + $configuration = $this->createConfigurationDefinition(); + + // commands + if (class_exists('Symfony\Component\Console\Command\Command')) { + $this->createSymfonyCommandDefinitions($driver, $configuration); } } @@ -110,14 +109,66 @@ public function beforeCompile() $factory->arguments = ["@$conn"]; } - // diff generators - if ($config['diffGenerator'] === TRUE && $builder->findByType('Doctrine\ORM\EntityManager')) { - $this->configureDoctrineStructureDiffGenerator(); + // diff generator + if ($config['diffGenerator'] === TRUE) { + if ($builder->findByType('Doctrine\ORM\EntityManager') && $builder->hasDefinition($this->prefix('group.structures'))) { + Validators::assertField($config, 'ignoredQueriesFile', 'null|string'); + $diffGenerator = $this->createDoctrineStructureDiffGeneratorDefinition($config['ignoredQueriesFile']); + $builder->getDefinition($this->prefix('group.structures')) + ->addSetup('$generator', [$diffGenerator]); + } + } + + // configuration + $groups = []; + foreach ($builder->findByTag(self::TAG_GROUP) as $serviceName => $_) { + $groups[] = $builder->getDefinition($serviceName); } + + $extensionHandlers = []; + foreach ($builder->findByTag(self::TAG_EXTENSION_HANDLER) as $serviceName => $extensionName) { + $extensionHandlers[$extensionName] = $builder->getDefinition($serviceName); + } + + $builder->getDefinition($this->prefix('configuration')) + ->setArguments([$groups, $extensionHandlers]); } - private function getDriver($driver, $dbal) + private function getDbalDefinition($dbal) + { + $factory = $this->getDbalFactory($dbal); + + if ($factory) { + return $this->getContainerBuilder() + ->addDefinition($this->prefix('dbal')) + ->setClass('Nextras\Migrations\IDbal') + ->setFactory($factory); + + } elseif ($dbal === NULL) { + return '@Nextras\Migrations\IDbal'; + + } else { + throw new Nextras\Migrations\LogicException('Invalid dbal value'); + } + } + + + private function getDbalFactory($dbal) + { + if ($dbal instanceof Nette\DI\Statement) { + return Nette\DI\Compiler::filterArguments([$dbal])[0]; + + } elseif (is_string($dbal) && isset($this->dbals[$dbal])) { + return $this->dbals[$dbal]; + + } else { + return NULL; + } + } + + + private function getDriverDefinition($driver, $dbal) { $factory = $this->getDriverFactory($driver, $dbal); @@ -150,54 +201,141 @@ private function getDriverFactory($driver, $dbal) } - private function getDbal($dbal) + private function createDefaultGroupConfiguration($dir, $withDummyData) { - $factory = $this->getDbalFactory($dbal); + $builder = $this->getContainerBuilder(); - if ($factory) { - return $this->getContainerBuilder() - ->addDefinition($this->prefix('dbal')) - ->setClass('Nextras\Migrations\IDbal') - ->setFactory($factory); + $groups = [ + 'structures' => [ + 'directory' => "$dir/structures", + ], + 'basic-data' => [ + 'directory' => "$dir/basic-data", + 'dependencies' => ['structures'], + ], + 'dummy-data' => [ + 'enabled' => $withDummyData, + 'directory' => "$dir/dummy-data", + 'dependencies' => ['structures', 'basic-data'], + ], + ]; + + foreach ($groups as $groupName => $groupConfig) { + $serviceName = $this->prefix("diffGenerator.$groupName"); + $diffGenerator = $builder->hasDefinition($serviceName) ? $builder->getDefinition($serviceName) : NULL; + $groups[$groupName]['generator'] = $diffGenerator; + } - } elseif ($dbal === NULL) { - return '@Nextras\Migrations\IDbal'; + return $groups; + } - } else { - throw new Nextras\Migrations\LogicException('Invalid dbal value'); + + private function createGroupDefinitions(array $groups) + { + /** @var IMigrationGroupsProvider $provider */ + foreach ($this->compiler->getExtensions('Nextras\Migrations\Bridges\NetteDI\IMigrationGroupsProvider') as $provider) { + foreach ($provider->getMigrationGroups() as $group) { + $groups[$group->name] = [ + 'enabled' => $group->enabled, + 'directory' => $group->directory, + 'dependencies' => $group->dependencies, + 'generator' => $group->generator, + ]; + } + } + + $builder = $this->getContainerBuilder(); + $groupDefinitions = []; + + foreach ($groups as $groupName => $groupConfig) { + Validators::assertField($groupConfig, 'directory', 'string'); + + $enabled = isset($groupConfig['enabled']) ? $groupConfig['enabled'] : true; + $directory = $groupConfig['directory']; + $dependencies = isset($groupConfig['dependencies']) ? $groupConfig['dependencies'] : []; + $generator = isset($groupConfig['generator']) ? $groupConfig['generator'] : null; + + $serviceName = lcfirst(str_replace('-', '', ucwords($groupName, '-'))); + $groupDefinitions[] = $builder->addDefinition($this->prefix("group.$serviceName")) + ->addTag(self::TAG_GROUP) + ->setAutowired(FALSE) + ->setClass('Nextras\Migrations\Entities\Group') + ->addSetup('$name', [$groupName]) + ->addSetup('$enabled', [$enabled]) + ->addSetup('$directory', [$directory]) + ->addSetup('$dependencies', [$dependencies]) + ->addSetup('$generator', [$generator]); } + + return $groupDefinitions; } - private function getDbalFactory($dbal) + private function createExtensionHandlerDefinitions($driver, $phpParams) { - if ($dbal instanceof Nette\DI\Statement) { - return Nette\DI\Compiler::filterArguments([$dbal])[0]; + $builder = $this->getContainerBuilder(); - } elseif (is_string($dbal) && isset($this->dbals[$dbal])) { - return $this->dbals[$dbal]; + $sqlHandler = $builder->addDefinition($this->prefix('extensionHandler.sql')) + ->addTag(self::TAG_EXTENSION_HANDLER, 'sql') + ->setAutowired(FALSE) + ->setClass('Nextras\Migrations\Extensions\SqlHandler') + ->setArguments([$driver]); - } else { - return NULL; - } + $phpHandler = $builder->addDefinition($this->prefix('extensionHandler.php')) + ->addTag(self::TAG_EXTENSION_HANDLER, 'php') + ->setClass('Nextras\Migrations\Extensions\PhpHandler') + ->setAutowired(FALSE) + ->setArguments([$phpParams]); + + return [$sqlHandler, $phpHandler]; + } + + + private function createConfigurationDefinition() + { + return $this->getContainerBuilder() + ->addDefinition($this->prefix('configuration')) + ->setClass('Nextras\Migrations\IConfiguration') + ->setFactory('Nextras\Migrations\Configurations\Configuration'); } - private function configureDoctrineStructureDiffGenerator() + private function createDoctrineStructureDiffGeneratorDefinition($ignoredQueriesFile) { $builder = $this->getContainerBuilder(); - $config = $this->validateConfig($this->defaults); - $structureDiffGenerator = $builder->getDefinition($this->prefix('structureDiffGenerator')) - ->setDynamic(FALSE) + return $builder->addDefinition($this->prefix('diffGenerator.structures')) + ->setAutowired(FALSE) + ->setClass('Nextras\Migrations\IDiffGenerator') ->setFactory('Nextras\Migrations\Bridges\DoctrineOrm\StructureDiffGenerator') - ->setArguments([ - '@Doctrine\ORM\EntityManager', - $config['ignoredQueriesFile'] - ]); + ->setArguments(['@Doctrine\ORM\EntityManager', $ignoredQueriesFile]); + } + + + private function createSymfonyCommandDefinitions($driver, $configuration) + { + $builder = $this->getContainerBuilder(); + $builder->addExcludedClasses(['Nextras\Migrations\Bridges\SymfonyConsole\BaseCommand']); - $configuration = $builder->getDefinition($this->prefix('configuration')); - $configuration->addSetup('setStructureDiffGenerator', [$structureDiffGenerator]); + $builder->addDefinition($this->prefix('continueCommand')) + ->setClass('Nextras\Migrations\Bridges\SymfonyConsole\ContinueCommand') + ->setArguments([$driver, $configuration]) + ->addTag('kdyby.console.command'); + + $builder->addDefinition($this->prefix('createCommand')) + ->setClass('Nextras\Migrations\Bridges\SymfonyConsole\CreateCommand') + ->setArguments([$driver, $configuration]) + ->addTag('kdyby.console.command'); + + $builder->addDefinition($this->prefix('resetCommand')) + ->setClass('Nextras\Migrations\Bridges\SymfonyConsole\ResetCommand') + ->setArguments([$driver, $configuration]) + ->addTag('kdyby.console.command'); + + $builder->addDefinition($this->prefix('statusCommand')) + ->setClass('Nextras\Migrations\Bridges\SymfonyConsole\StatusCommand') + ->setArguments([$driver, $configuration]) + ->addTag('kdyby.console.command'); } } diff --git a/src/Bridges/NextrasDbal/NextrasAdapter.php b/src/Bridges/NextrasDbal/NextrasAdapter.php index 9838d71..e6a127e 100644 --- a/src/Bridges/NextrasDbal/NextrasAdapter.php +++ b/src/Bridges/NextrasDbal/NextrasAdapter.php @@ -61,7 +61,7 @@ public function escapeString($value) public function escapeInt($value) { - return (int) $value; + return (string) (int) $value; } diff --git a/src/Bridges/SymfonyBundle/DependencyInjection/Configuration.php b/src/Bridges/SymfonyBundle/DependencyInjection/Configuration.php new file mode 100644 index 0000000..bdf88f1 --- /dev/null +++ b/src/Bridges/SymfonyBundle/DependencyInjection/Configuration.php @@ -0,0 +1,52 @@ +root('nextras_migrations')->requiresAtLeastOneElement()->children() + ->scalarNode('dir') + ->defaultValue('%kernel.root_dir%/NextrasMigrations') + ->cannotBeEmpty() + ->end() + ->enumNode('dbal') + ->values(['dibi', 'dibi2', 'dibi3', 'doctrine', 'nette', 'nextras']) + ->defaultValue('doctrine') + ->cannotBeEmpty() + ->end() + ->enumNode('driver') + ->values(['mysql', 'pgsql']) + ->cannotBeEmpty() + ->end() + ->scalarNode('diff_generator') + ->defaultValue('doctrine') + ->end() + ->booleanNode('with_dummy_data') + ->defaultFalse() + ->end() + ->arrayNode('php_params') + ->variablePrototype() + ->end() + ->end() + ->scalarNode('ignored_queries_file') + ->defaultNull() + ->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/src/Bridges/SymfonyBundle/DependencyInjection/NextrasMigrationsExtension.php b/src/Bridges/SymfonyBundle/DependencyInjection/NextrasMigrationsExtension.php new file mode 100644 index 0000000..71a4ae9 --- /dev/null +++ b/src/Bridges/SymfonyBundle/DependencyInjection/NextrasMigrationsExtension.php @@ -0,0 +1,103 @@ + 'Nextras\Migrations\Bridges\Dibi\DibiAdapter', + 'dibi2' => 'Nextras\Migrations\Bridges\Dibi\Dibi2Adapter', + 'dibi3' => 'Nextras\Migrations\Bridges\Dibi\Dibi3Adapter', + 'doctrine' => 'Nextras\Migrations\Bridges\DoctrineDbal\DoctrineAdapter', + 'nette' => 'Nextras\Migrations\Bridges\NetteDatabase\NetteAdapter', + 'nextras' => 'Nextras\Migrations\Bridges\NextrasDbal\NextrasAdapter', + ]; + + /** @var array */ + protected $drivers = [ + 'mysql' => 'Nextras\Migrations\Drivers\MySqlDriver', + 'pgsql' => 'Nextras\Migrations\Drivers\PgSqlDriver', + ]; + + + public function load(array $configs, ContainerBuilder $container) + { + $config = $this->processConfiguration(new Configuration(), $configs); + + $dbalAlias = $config['dbal']; + $dbalDefinition = new Definition($this->dbals[$dbalAlias]); + $dbalDefinition->setAutowired(TRUE); + + $driverAlias = $config['driver']; + $driverDefinition = new Definition($this->drivers[$driverAlias]); + $driverDefinition->setAutowired(TRUE); + + $container->addDefinitions([ + 'nextras_migrations.dbal' => $dbalDefinition, + 'nextras_migrations.driver' => $driverDefinition, + ]); + + if ($config['diff_generator'] === 'doctrine') { + $structureDiffGeneratorDefinition = new Definition('Nextras\Migrations\Bridges\DoctrineOrm\StructureDiffGenerator'); + $structureDiffGeneratorDefinition->setAutowired(TRUE); + $structureDiffGeneratorDefinition->setArgument('$ignoredQueriesFile', $config['ignored_queries_file']); + + } else { + $structureDiffGeneratorDefinition = NULL; + } + + foreach ($config['php_params'] as $phpParamKey => $phpParamValue) { + if (is_string($phpParamValue) && strlen($phpParamValue) > 1 && $phpParamValue[0] === '@') { + $serviceName = substr($phpParamValue, 1); + $config['php_params'][$phpParamKey] = $container->getDefinition($serviceName); + } + } + + $configurationDefinition = new Definition('Nextras\Migrations\Configurations\DefaultConfiguration'); + $configurationDefinition->setArguments([$config['dir'], $driverDefinition, $config['with_dummy_data'], $config['php_params']]); + $configurationDefinition->addMethodCall('setStructureDiffGenerator', [$structureDiffGeneratorDefinition]); + + $continueCommandDefinition = new Definition('Nextras\Migrations\Bridges\SymfonyConsole\ContinueCommand'); + $continueCommandDefinition->setAutowired(TRUE); + $continueCommandDefinition->addTag('console.command'); + + $createCommandDefinition = new Definition('Nextras\Migrations\Bridges\SymfonyConsole\CreateCommand'); + $createCommandDefinition->setAutowired(TRUE); + $createCommandDefinition->addTag('console.command'); + + $resetCommandDefinition = new Definition('Nextras\Migrations\Bridges\SymfonyConsole\ResetCommand'); + $resetCommandDefinition->setAutowired(TRUE); + $resetCommandDefinition->addTag('console.command'); + + $statusCommandDefinition = new Definition('Nextras\Migrations\Bridges\SymfonyConsole\StatusCommand'); + $statusCommandDefinition->setAutowired(TRUE); + $statusCommandDefinition->addTag('console.command'); + + $container->addDefinitions([ + 'nextras_migrations.configuration' => $configurationDefinition, + 'nextras_migrations.continue_command' => $continueCommandDefinition, + 'nextras_migrations.create_command' => $createCommandDefinition, + 'nextras_migrations.reset_command' => $resetCommandDefinition, + 'nextras_migrations.status_command' => $statusCommandDefinition, + ]); + + if ($structureDiffGeneratorDefinition) { + $container->addDefinitions([ + 'nextras_migrations.structure_diff_generator' => $structureDiffGeneratorDefinition, + ]); + } + } +} diff --git a/src/Bridges/SymfonyBundle/NextrasMigrationsBundle.php b/src/Bridges/SymfonyBundle/NextrasMigrationsBundle.php new file mode 100644 index 0000000..6f3c61d --- /dev/null +++ b/src/Bridges/SymfonyBundle/NextrasMigrationsBundle.php @@ -0,0 +1,18 @@ +setName('migrations:continue'); + $this->setName(self::$defaultName); $this->setDescription('Updates database schema by running all new migrations'); $this->setHelp("If table 'migrations' does not exist in current database, it is created automatically."); } diff --git a/src/Bridges/SymfonyConsole/CreateCommand.php b/src/Bridges/SymfonyConsole/CreateCommand.php index 85e5069..096a0e5 100644 --- a/src/Bridges/SymfonyConsole/CreateCommand.php +++ b/src/Bridges/SymfonyConsole/CreateCommand.php @@ -25,6 +25,9 @@ class CreateCommand extends BaseCommand const CONTENT_SOURCE_STDIN = 'stdin'; const CONTENT_SOURCE_EMPTY = 'empty'; + /** @var string */ + protected static $defaultName = 'migrations:create'; + /** @var string */ protected $defaultContentSource = self::CONTENT_SOURCE_DIFF; @@ -44,7 +47,7 @@ public function setDefaultContentSource($defaultContentSource) */ protected function configure() { - $this->setName('migrations:create'); + $this->setName(self::$defaultName); $this->setDescription('Creates new migration file with proper name (e.g. 2015-03-14-130836-label.sql)'); $this->setHelp('Prints path of the created file to standard output.'); @@ -112,7 +115,8 @@ protected function getGroup($type) } } - throw new Nextras\Migrations\LogicException("Unknown type '$type' given, expected on of 's', 'b' or 'd'."); + $types = $this->getTypeArgDescription(); + throw new Nextras\Migrations\LogicException("Unknown type '$type' given, expected one of $types."); } @@ -135,10 +139,13 @@ protected function getFileName($label, $extension) protected function hasNumericSubdirectory($dir, & $found) { $items = @scandir($dir); // directory may not exist - foreach ($items as $item) { - if ($item !== '.' && $item !== '..' && is_dir($dir . '/' . $item)) { - $found = $dir . '/' . $item; - return TRUE; + + if ($items) { + foreach ($items as $item) { + if ($item !== '.' && $item !== '..' && is_dir($dir . '/' . $item)) { + $found = $dir . '/' . $item; + return TRUE; + } } } diff --git a/src/Bridges/SymfonyConsole/ResetCommand.php b/src/Bridges/SymfonyConsole/ResetCommand.php index 05b91a2..930cf0a 100644 --- a/src/Bridges/SymfonyConsole/ResetCommand.php +++ b/src/Bridges/SymfonyConsole/ResetCommand.php @@ -16,9 +16,12 @@ class ResetCommand extends BaseCommand { + /** @var string */ + protected static $defaultName = 'migrations:reset'; + protected function configure() { - $this->setName('migrations:reset'); + $this->setName(self::$defaultName); $this->setDescription('Drops current database and recreates it from scratch'); $this->setHelp("Drops current database and runs all migrations"); } diff --git a/src/Bridges/SymfonyConsole/StatusCommand.php b/src/Bridges/SymfonyConsole/StatusCommand.php new file mode 100644 index 0000000..ad81bb0 --- /dev/null +++ b/src/Bridges/SymfonyConsole/StatusCommand.php @@ -0,0 +1,35 @@ +setName(self::$defaultName); + $this->setDescription('Show lists of completed or waiting migrations'); + } + + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->runMigrations(Runner::MODE_STATUS, $this->config); + } + +} diff --git a/src/Configurations/Configuration.php b/src/Configurations/Configuration.php new file mode 100644 index 0000000..b963644 --- /dev/null +++ b/src/Configurations/Configuration.php @@ -0,0 +1,57 @@ + IExtensionHandler) */ + private $extensionHandlers; + + + /** + * @param Group[] $groups + * @param IExtensionHandler[] $extensionHandlers (extension => IExtensionHandler) + */ + public function __construct(array $groups, array $extensionHandlers) + { + $this->groups = $groups; + $this->extensionHandlers = $extensionHandlers; + } + + + /** + * @return Group[] + */ + public function getGroups() + { + return $this->groups; + } + + + /** + * @return IExtensionHandler[] (extension => IExtensionHandler) + */ + public function getExtensionHandlers() + { + return $this->extensionHandlers; + } +} diff --git a/src/Configurations/DefaultConfiguration.php b/src/Configurations/DefaultConfiguration.php index d8060fe..c38819b 100644 --- a/src/Configurations/DefaultConfiguration.php +++ b/src/Configurations/DefaultConfiguration.php @@ -20,6 +20,7 @@ /** * @author Jan Tvrdík + * @deprecated */ class DefaultConfiguration implements IConfiguration { diff --git a/src/Drivers/BaseDriver.php b/src/Drivers/BaseDriver.php index 6abac88..ff48771 100644 --- a/src/Drivers/BaseDriver.php +++ b/src/Drivers/BaseDriver.php @@ -27,6 +27,9 @@ abstract class BaseDriver implements IDriver /** @var string */ protected $tableName; + /** @var NULL|string */ + protected $tableNameQuoted; + /** * @param IDbal $dbal @@ -35,7 +38,13 @@ abstract class BaseDriver implements IDriver public function __construct(IDbal $dbal, $tableName = 'migrations') { $this->dbal = $dbal; - $this->tableName = $dbal->escapeIdentifier($tableName); + $this->tableName = $tableName; + } + + + public function setupConnection() + { + $this->tableNameQuoted = $this->dbal->escapeIdentifier($this->tableName); } @@ -63,9 +72,9 @@ public function loadFile($path) $queries = 0; $space = "(?:\\s|/\\*.*\\*/|(?:#|-- )[^\\n]*\\n|--\\n)"; - $spacesRe = "~^{$space}*\\z~"; + $spacesRe = "~\\G{$space}*\\z~"; $delimiter = ';'; - $delimiterRe = "~^{$space}*DELIMITER\\s+(\\S+)~i"; + $delimiterRe = "~\\G{$space}*DELIMITER\\s+(\\S+)~i"; $openRe = $this instanceof PgSqlDriver ? '[\'"]|/\*|-- |\z|\$[^$]*\$' : '[\'"`#]|/\*|-- |\z'; $parseRe = "(;|$openRe)"; @@ -104,7 +113,7 @@ public function loadFile($path) } } else { // last query or EOF - if (preg_match($spacesRe, substr($content, $queryOffset))) { + if (preg_match($spacesRe, $content, $_, 0, $queryOffset)) { break 2; } else { diff --git a/src/Drivers/MySqlDriver.php b/src/Drivers/MySqlDriver.php index 7c961e7..11d69c1 100644 --- a/src/Drivers/MySqlDriver.php +++ b/src/Drivers/MySqlDriver.php @@ -24,7 +24,8 @@ class MySqlDriver extends BaseDriver implements IDriver { public function setupConnection() { - $this->dbal->exec('SET NAMES "utf8"'); + parent::setupConnection(); + $this->dbal->exec('SET NAMES "utf8mb4"'); $this->dbal->exec('SET foreign_key_checks = 0'); $this->dbal->exec('SET time_zone = "SYSTEM"'); $this->dbal->exec('SET sql_mode = "TRADITIONAL"'); @@ -91,14 +92,14 @@ public function createTable() public function dropTable() { - $this->dbal->exec("DROP TABLE {$this->tableName}"); + $this->dbal->exec("DROP TABLE {$this->tableNameQuoted}"); } public function insertMigration(Migration $migration) { $this->dbal->exec(" - INSERT INTO {$this->tableName} + INSERT INTO {$this->tableNameQuoted} (`group`, `file`, `checksum`, `executed`, `ready`) VALUES (" . $this->dbal->escapeString($migration->group) . "," . $this->dbal->escapeString($migration->filename) . "," . @@ -115,7 +116,7 @@ public function insertMigration(Migration $migration) public function markMigrationAsReady(Migration $migration) { $this->dbal->exec(" - UPDATE {$this->tableName} + UPDATE {$this->tableNameQuoted} SET `ready` = 1 WHERE `id` = " . $this->dbal->escapeInt($migration->id) ); @@ -125,7 +126,7 @@ public function markMigrationAsReady(Migration $migration) public function getAllMigrations() { $migrations = array(); - $result = $this->dbal->query("SELECT * FROM {$this->tableName} ORDER BY `executed`"); + $result = $this->dbal->query("SELECT * FROM {$this->tableNameQuoted} ORDER BY `executed`"); foreach ($result as $row) { $migration = new Migration; $migration->id = (int) $row['id']; @@ -145,7 +146,7 @@ public function getAllMigrations() public function getInitTableSource() { return preg_replace('#^\t{3}#m', '', trim(" - CREATE TABLE IF NOT EXISTS {$this->tableName} ( + CREATE TABLE IF NOT EXISTS {$this->tableNameQuoted} ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `group` varchar(100) NOT NULL, `file` varchar(100) NOT NULL, @@ -163,7 +164,7 @@ public function getInitMigrationsSource(array $files) { $out = ''; foreach ($files as $file) { - $out .= "INSERT INTO {$this->tableName} "; + $out .= "INSERT INTO {$this->tableNameQuoted} "; $out .= "(`group`, `file`, `checksum`, `executed`, `ready`) VALUES (" . $this->dbal->escapeString($file->group->name) . ", " . $this->dbal->escapeString($file->name) . ", " . diff --git a/src/Drivers/PgSqlDriver.php b/src/Drivers/PgSqlDriver.php index 268f1f3..63ea8a9 100644 --- a/src/Drivers/PgSqlDriver.php +++ b/src/Drivers/PgSqlDriver.php @@ -26,14 +26,8 @@ class PgSqlDriver extends BaseDriver implements IDriver /** @var string */ protected $schema; - /** @var string */ - protected $schemaStr; - - /** @var string */ - protected $primarySequence; - - /** @var string */ - protected $lockTableName; + /** @var NULL|string */ + protected $schemaQuoted; /** @@ -44,22 +38,21 @@ class PgSqlDriver extends BaseDriver implements IDriver public function __construct(IDbal $dbal, $tableName = 'migrations', $schema = 'public') { parent::__construct($dbal, $tableName); - $this->schema = $dbal->escapeIdentifier($schema); - $this->schemaStr = $dbal->escapeString($schema); - $this->primarySequence = $this->dbal->escapeString($tableName . '_id_seq'); - $this->lockTableName = $dbal->escapeIdentifier($tableName . '_lock'); + $this->schema = $schema; } public function setupConnection() { + parent::setupConnection(); + $this->schemaQuoted = $this->dbal->escapeIdentifier($this->schema); } public function emptyDatabase() { - $this->dbal->exec("DROP SCHEMA IF EXISTS {$this->schema} CASCADE"); - $this->dbal->exec("CREATE SCHEMA {$this->schema}"); + $this->dbal->exec("DROP SCHEMA IF EXISTS {$this->schemaQuoted} CASCADE"); + $this->dbal->exec("CREATE SCHEMA {$this->schemaQuoted}"); } @@ -84,18 +77,8 @@ public function rollbackTransaction() public function lock() { try { - $schemaExist = (bool) $this->dbal->query(" - SELECT schema_name - FROM information_schema.schemata - WHERE schema_name = {$this->schemaStr} - "); - - if (!$schemaExist) { - // CREATE SCHEMA IF NOT EXIST is not available in PostgreSQL < 9.3 - $this->dbal->exec("CREATE SCHEMA {$this->schema}"); - } - - $this->dbal->exec("CREATE TABLE {$this->schema}.{$this->lockTableName} (\"foo\" INT)"); + $this->dbal->exec('SELECT pg_advisory_lock(-2099128779216184107)'); + } catch (\Exception $e) { throw new LockException('Unable to acquire a lock.', NULL, $e); } @@ -105,7 +88,8 @@ public function lock() public function unlock() { try { - $this->dbal->exec("DROP TABLE IF EXISTS {$this->schema}.{$this->lockTableName}"); + $this->dbal->exec('SELECT pg_advisory_unlock(-2099128779216184107)'); + } catch (\Exception $e) { throw new LockException('Unable to release a lock.', NULL, $e); } @@ -120,14 +104,14 @@ public function createTable() public function dropTable() { - $this->dbal->exec("DROP TABLE {$this->schema}.{$this->tableName}"); + $this->dbal->exec("DROP TABLE {$this->schemaQuoted}.{$this->tableNameQuoted}"); } public function insertMigration(Migration $migration) { - $this->dbal->exec(" - INSERT INTO {$this->schema}.{$this->tableName}" . ' + $rows = $this->dbal->query(" + INSERT INTO {$this->schemaQuoted}.{$this->tableNameQuoted}" . ' ("group", "file", "checksum", "executed", "ready") VALUES (' . $this->dbal->escapeString($migration->group) . "," . $this->dbal->escapeString($migration->filename) . "," . @@ -135,16 +119,17 @@ public function insertMigration(Migration $migration) $this->dbal->escapeDateTime($migration->executedAt) . "," . $this->dbal->escapeBool(FALSE) . ") + RETURNING id "); - $migration->id = (int) $this->dbal->query('SELECT CURRVAL('. $this->primarySequence . ') AS id')[0]['id']; + $migration->id = (int) $rows[0]['id']; } public function markMigrationAsReady(Migration $migration) { $this->dbal->exec(" - UPDATE {$this->schema}.{$this->tableName}" . ' + UPDATE {$this->schemaQuoted}.{$this->tableNameQuoted}" . ' SET "ready" = TRUE WHERE "id" = ' . $this->dbal->escapeInt($migration->id) ); @@ -154,7 +139,7 @@ public function markMigrationAsReady(Migration $migration) public function getAllMigrations() { $migrations = array(); - $result = $this->dbal->query("SELECT * FROM {$this->schema}.{$this->tableName} ORDER BY \"executed\""); + $result = $this->dbal->query("SELECT * FROM {$this->schemaQuoted}.{$this->tableNameQuoted} ORDER BY \"executed\""); foreach ($result as $row) { $migration = new Migration; $migration->id = (int) $row['id']; @@ -174,7 +159,7 @@ public function getAllMigrations() public function getInitTableSource() { return preg_replace('#^\t{3}#m', '', trim(" - CREATE TABLE IF NOT EXISTS {$this->schema}.{$this->tableName} (" . ' + CREATE TABLE IF NOT EXISTS {$this->schemaQuoted}.{$this->tableNameQuoted} (" . ' "id" serial4 NOT NULL, "group" varchar(100) NOT NULL, "file" varchar(100) NOT NULL, @@ -192,7 +177,7 @@ public function getInitMigrationsSource(array $files) { $out = ''; foreach ($files as $file) { - $out .= "INSERT INTO {$this->schema}.{$this->tableName} "; + $out .= "INSERT INTO {$this->schemaQuoted}.{$this->tableNameQuoted} "; $out .= '("group", "file", "checksum", "executed", "ready") VALUES (' . $this->dbal->escapeString($file->group->name) . ", " . $this->dbal->escapeString($file->name) . ", " . diff --git a/src/Engine/Finder.php b/src/Engine/Finder.php index 5944355..607da94 100644 --- a/src/Engine/Finder.php +++ b/src/Engine/Finder.php @@ -144,15 +144,10 @@ protected function getFilesRecursive($dir) /** * @param string $dir * @return array - * @throws IOException */ protected function getItems($dir) { - $items = @scandir($dir); // directory may not exist - if ($items === FALSE) { - throw new IOException(sprintf('Finder: Directory "%s" does not exist.', $dir)); - } - return $items; + return @scandir($dir) ?: []; // directory may not exist } } diff --git a/src/Engine/OrderResolver.php b/src/Engine/OrderResolver.php index 6e92656..d0a80b7 100644 --- a/src/Engine/OrderResolver.php +++ b/src/Engine/OrderResolver.php @@ -27,13 +27,13 @@ class OrderResolver */ public function resolve(array $migrations, array $groups, array $files, $mode) { + $this->checkModeSupport($mode); + $groups = $this->getAssocGroups($groups); $this->validateGroups($groups); if ($mode === Runner::MODE_RESET) { return $this->sortFiles($files, $groups); - } elseif ($mode !== Runner::MODE_CONTINUE) { - throw new LogicException('Unsupported mode.'); } $migrations = $this->getAssocMigrations($migrations); @@ -254,5 +254,21 @@ private function validateGroups(array $groups) } } } - + + /** + * @param string $mode + */ + private function checkModeSupport($mode) + { + $supportedModes = [ + Runner::MODE_CONTINUE, + Runner::MODE_RESET, + Runner::MODE_STATUS, + ]; + + if (!in_array($mode, $supportedModes, true)) { + throw new LogicException('Unsupported mode.'); + } + } + } diff --git a/src/Engine/Runner.php b/src/Engine/Runner.php index 7e65a69..b9c34b0 100644 --- a/src/Engine/Runner.php +++ b/src/Engine/Runner.php @@ -19,6 +19,7 @@ use Nextras\Migrations\IDriver; use Nextras\Migrations\IExtensionHandler; use Nextras\Migrations\IPrinter; +use Nextras\Migrations\LockException; use Nextras\Migrations\LogicException; @@ -28,6 +29,8 @@ class Runner const MODE_CONTINUE = 'continue'; const MODE_RESET = 'reset'; const MODE_INIT = 'init'; + const MODE_STATUS = 'status'; + /** @var IPrinter */ private $printer; @@ -78,12 +81,14 @@ public function addExtensionHandler($extension, IExtensionHandler $handler) $this->extensionsHandlers[$extension] = $handler; return $this; } - - + + /** - * @param string $mode self::MODE_CONTINUE|self::MODE_RESET|self::MODE_INIT + * @param string $mode self::MODE_CONTINUE|self::MODE_RESET|self::MODE_INIT|self::MODE_STATUS * @param IConfiguration $config + * * @return void + * @throws Exception */ public function run($mode = self::MODE_CONTINUE, IConfiguration $config = NULL) { @@ -119,17 +124,25 @@ public function run($mode = self::MODE_CONTINUE, IConfiguration $config = NULL) $migrations = $this->driver->getAllMigrations(); $files = $this->finder->find($this->groups, array_keys($this->extensionsHandlers)); $toExecute = $this->orderResolver->resolve($migrations, $this->groups, $files, $mode); - $this->printer->printToExecute($toExecute); - - foreach ($toExecute as $file) { - $time = microtime(TRUE); - $queriesCount = $this->execute($file); - $this->printer->printExecute($file, $queriesCount, microtime(TRUE) - $time); + + if ($mode === self::MODE_STATUS) { + $this->printer->printExecutedMigrations($migrations); + $this->printer->printToExecute($toExecute, true); + } else { + $this->printer->printToExecute($toExecute, false); + foreach ($toExecute as $file) { + $time = microtime(TRUE); + $queriesCount = $this->execute($file); + $this->printer->printExecute($file, $queriesCount, microtime(TRUE) - $time); + } } $this->driver->unlock(); $this->printer->printDone(); + } catch (LockException $e) { + $this->printer->printError($e); + } catch (Exception $e) { $this->driver->unlock(); $this->printer->printError($e); diff --git a/src/Entities/Group.php b/src/Entities/Group.php index a7655a9..aa66581 100644 --- a/src/Entities/Group.php +++ b/src/Entities/Group.php @@ -21,15 +21,80 @@ class Group public $name; /** @var bool */ - public $enabled; + public $enabled = true; /** @var string absolute path do directory */ public $directory; /** @var string[] */ - public $dependencies; + public $dependencies = []; /** @var IDiffGenerator|NULL */ public $generator; + + /** + * @param null|string $name + * @param null|string $directory + */ + public function __construct($name = null, $directory = null) + { + $this->name = $name; + $this->directory = $directory; + } + + + /** + * @param string $name + * @return static + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + + /** + * @param bool $enabled + * @return static + */ + public function setEnabled($enabled) + { + $this->enabled = $enabled; + return $this; + } + + + /** + * @param string $directory + * @return static + */ + public function setDirectory($directory) + { + $this->directory = $directory; + return $this; + } + + + /** + * @param string[] $dependencies + * @return static + */ + public function setDependencies(array $dependencies) + { + $this->dependencies = $dependencies; + return $this; + } + + + /** + * @param IDiffGenerator|NULL $generator + * @return static + */ + public function setGenerator(IDiffGenerator $generator = NULL) + { + $this->generator = $generator; + return $this; + } } diff --git a/src/IPrinter.php b/src/IPrinter.php index 586bc3c..a64d8f6 100644 --- a/src/IPrinter.php +++ b/src/IPrinter.php @@ -10,6 +10,7 @@ namespace Nextras\Migrations; use Nextras\Migrations\Entities\File; +use Nextras\Migrations\Entities\Migration; /** @@ -23,14 +24,25 @@ interface IPrinter * - continue = Running new migrations. * @param string $mode */ - function printIntro($mode); - - + public function printIntro($mode); + + /** + * List of migrations which executed has been completed. + * @param Migration[] $migrations + * + * @return void + */ + public function printExecutedMigrations(array $migrations); + /** * List of migrations which should be executed has been completed. + * * @param File[] $toExecute + * @param bool $withFileList + * + * @return void */ - function printToExecute(array $toExecute); + public function printToExecute(array $toExecute, $withFileList = false); /** @@ -39,26 +51,26 @@ function printToExecute(array $toExecute); * @param int $count number of executed queries * @param float $time elapsed time in milliseconds */ - function printExecute(File $file, $count, $time); + public function printExecute(File $file, $count, $time); /** * All migrations have been successfully executed. */ - function printDone(); + public function printDone(); /** * An error has occurred during execution of a migration. * @param Exception $e */ - function printError(Exception $e); + public function printError(Exception $e); /** * Prints init source code. * @param string $code */ - function printSource($code); + public function printSource($code); } diff --git a/src/Printers/Console.php b/src/Printers/Console.php index 72d1698..00e4deb 100644 --- a/src/Printers/Console.php +++ b/src/Printers/Console.php @@ -10,6 +10,7 @@ namespace Nextras\Migrations\Printers; use Nextras\Migrations\Entities\File; +use Nextras\Migrations\Entities\Migration; use Nextras\Migrations\Exception; use Nextras\Migrations\IPrinter; @@ -34,42 +35,80 @@ public function __construct() { $this->useColors = $this->detectColorSupport(); } - - + + /** + * @inheritdoc + */ public function printIntro($mode) { $this->output('Nextras Migrations'); $this->output(strtoupper($mode), self::COLOR_INTRO); } - - - public function printToExecute(array $toExecute) + + /** + * @inheritdoc + */ + public function printExecutedMigrations(array $migrations) + { + if ($migrations) { + $this->output('Executed migrations:'); + /** @var Migration $migration */ + foreach ($migrations as $migration) { + $this->output('- ' . $migration->group . '/' . $migration->filename . ' OK', self::COLOR_SUCCESS); + } + $this->output(' '); + } else { + $this->output('No migrations has executed yet.'); + } + } + + /** + * @inheritdoc + */ + public function printToExecute(array $toExecute, $withFileList = FALSE) { if ($toExecute) { $count = count($toExecute); - $this->output($count . ' migration' . ($count > 1 ? 's' : '') . ' need' . ($count > 1 ? '' : 's') . ' to be executed.'); + $this->output( + sprintf( + '%s migration%s need%s to be executed%s', + $count, $count > 1 ? 's' : '', $count > 1 ? '' : 's', ($withFileList ? ':' : '.') + ) + ); + if ($withFileList) { + /** @var File $file */ + foreach ($toExecute as $file) { + $this->output('- ' . $file->group->name . '/' . $file->name, self::COLOR_INFO); + } + } } else { $this->output('No migration needs to be executed.'); } } - - + + /** + * @inheritdoc + */ public function printExecute(File $file, $count, $time) { $this->output( '- ' . $file->group->name . '/' . $file->name . '; ' . $this->color($count, self::COLOR_INFO) . ' queries; ' - . $this->color(sprintf('%0.3f', $time), self::COLOR_INFO) . ' ms' + . $this->color(sprintf('%0.3f', $time), self::COLOR_INFO) . ' s' ); } - - + + /** + * @inheritdoc + */ public function printDone() { $this->output('OK', self::COLOR_SUCCESS); } - - + + /** + * @inheritdoc + */ public function printError(Exception $e) { $this->output('ERROR: ' . $e->getMessage(), self::COLOR_ERROR); diff --git a/src/Printers/DevNull.php b/src/Printers/DevNull.php index 5a26677..365b1e3 100644 --- a/src/Printers/DevNull.php +++ b/src/Printers/DevNull.php @@ -10,6 +10,7 @@ namespace Nextras\Migrations\Printers; use Nextras\Migrations\Entities\File; +use Nextras\Migrations\Entities\Migration; use Nextras\Migrations\Exception; use Nextras\Migrations\IPrinter; @@ -20,32 +21,54 @@ */ class DevNull implements IPrinter { + /** + * @inheritdoc + */ public function printIntro($mode) { } - - - public function printToExecute(array $toExecute) + + /** + * @inheritdoc + */ + public function printToExecute(array $toExecute, $withFileList = false) { } - - + + /** + * @inheritdoc + */ public function printExecute(File $file, $count, $time) { } - - + + /** + * @inheritdoc + */ public function printDone() { } - - + + /** + * @inheritdoc + */ public function printError(Exception $e) { } - - + + /** + * @inheritdoc + */ public function printSource($code) { } + + /** + * @inheritdoc + */ + public function printExecutedMigrations(array $migrations) + { + + } } + diff --git a/src/Printers/HtmlDump.php b/src/Printers/HtmlDump.php index 99b0cf2..6f7a9a7 100644 --- a/src/Printers/HtmlDump.php +++ b/src/Printers/HtmlDump.php @@ -11,6 +11,7 @@ use Nextras\Migrations\Engine\Runner; use Nextras\Migrations\Entities\File; +use Nextras\Migrations\Entities\Migration; use Nextras\Migrations\Exception; use Nextras\Migrations\IPrinter; @@ -25,69 +26,115 @@ class HtmlDump implements IPrinter /** @var int order of last executed migration */ private $index; - - + + /** + * @inheritdoc + */ public function printIntro($mode) { if ($mode === Runner::MODE_RESET) { $this->output(' RESET: All tables, views and data has been destroyed!'); - } else { + } + if ($mode === Runner::MODE_CONTINUE) { $this->output(' CONTINUE: Running only new migrations.'); } + if ($mode === Runner::MODE_STATUS) { + $this->output(' STATUS: Show lists of completed or waiting migrations'); + } } - - - public function printToExecute(array $toExecute) + + public function printExecutedMigrations(array $migrations) { + if ($migrations) { + $this->output('Executed migrations:'); + /** @var Migration $migration */ + foreach ($migrations as $migration) { + $this->output('- ' . $migration->group . '/' . $migration->filename . ' OK', 'success'); + } + $this->output(' '); + } else { + $this->output('No migrations has executed yet'); + } + } + + /** + * @inheritdoc + */ + public function printToExecute(array $toExecute, $withFileList = FALSE) + { + $count = 0; if ($toExecute) { - $this->output(' ' . count($toExecute) . ' migrations need to be executed.'); + $count = count($toExecute); + $this->output( + sprintf( + '%s migration%s need%s to be executed%s', + $count, $count > 1 ? 's' : '', $count > 1 ? '' : 's', ($withFileList ? ':' : '.') + ) + ); + if ($withFileList) { + /** @var File $file */ + foreach ($toExecute as $file) { + $this->output(' - ' . $file->group->name . '/' . $file->name); + } + } } else { $this->output('No migration needs to be executed.'); } - $this->count = count($toExecute); + $this->count = $count; $this->index = 0; } - - + + /** + * @inheritdoc + */ public function printExecute(File $file, $count, $time) { $format = '%0' . strlen($this->count) . 'd'; $name = htmlspecialchars($file->group->name . '/' . $file->name); - $this->output(sprintf( - $format . '/' . $format . ': %s (%d %s, %0.3f ms)', - ++$this->index, $this->count, $name, $count, ($count === 1 ? 'query' : 'queries'), $time - )); + $this->output( + sprintf( + $format . '/' . $format . ': %s (%d %s, %0.3f s)', + ++$this->index, $this->count, $name, $count, ($count === 1 ? 'query' : 'queries'), $time + ) + ); } - - + + /** + * @inheritdoc + */ public function printDone() { $this->output('OK', 'success'); } - - + + /** + * @inheritdoc + */ public function printError(Exception $e) { $this->output('ERROR: ' . htmlspecialchars($e->getMessage()), 'error'); throw $e; } - - + + /** + * @inheritdoc + */ public function printSource($code) { $this->output($code); } - - + + /** - * @param string $s HTML string + * @param string $s HTML string * @param string $class + * * @return void */ protected function output($s, $class = 'info') { echo "
$s
\n"; } - + } diff --git a/tests/cases/integration/MigrationsExtension.phpt b/tests/cases/integration/MigrationsExtension.phpt index 02074b8..f81aa26 100644 --- a/tests/cases/integration/MigrationsExtension.phpt +++ b/tests/cases/integration/MigrationsExtension.phpt @@ -25,8 +25,8 @@ class MigrationsExtensionTest extends TestCase $dic = $this->createContainer($config); Assert::type('Nextras\Migrations\Drivers\MySqlDriver', $dic->getByType('Nextras\Migrations\IDriver')); - Assert::count(3, $dic->findByType('Symfony\Component\Console\Command\Command')); - Assert::count(3, $dic->findByTag('kdyby.console.command')); + Assert::count(4, $dic->findByType('Symfony\Component\Console\Command\Command')); + Assert::count(4, $dic->findByTag('kdyby.console.command')); } @@ -51,7 +51,7 @@ class MigrationsExtensionTest extends TestCase $dic = $this->createContainer($config); $configuration = $dic->getByType('Nextras\Migrations\IConfiguration'); - Assert::type('Nextras\Migrations\Configurations\DefaultConfiguration', $configuration); + Assert::type('Nextras\Migrations\Configurations\Configuration', $configuration); $groups = $configuration->getGroups(); Assert::count(3, $groups); diff --git a/tests/cases/integration/Runner.FirstRun.phpt b/tests/cases/integration/Runner.FirstRun.phpt index 76c1546..0edeec4 100644 --- a/tests/cases/integration/Runner.FirstRun.phpt +++ b/tests/cases/integration/Runner.FirstRun.phpt @@ -24,11 +24,11 @@ class FirstRunTest extends IntegrationTestCase 'Nextras Migrations', 'RESET', '5 migrations need to be executed.', - '- structures/001.sql; 1 queries; XX ms', - '- structures/002.sql; 1 queries; XX ms', - '- basic-data/003.sql; 2 queries; XX ms', - '- dummy-data/004.sql; 1 queries; XX ms', - '- structures/005.sql; 1 queries; XX ms', + '- structures/001.sql; 1 queries; XX s', + '- structures/002.sql; 1 queries; XX s', + '- basic-data/003.sql; 2 queries; XX s', + '- dummy-data/004.sql; 1 queries; XX s', + '- structures/005.sql; 1 queries; XX s', 'OK', ], $this->printer->lines); @@ -62,11 +62,11 @@ class FirstRunTest extends IntegrationTestCase 'Nextras Migrations', 'CONTINUE', '5 migrations need to be executed.', - '- structures/001.sql; 1 queries; XX ms', - '- structures/002.sql; 1 queries; XX ms', - '- basic-data/003.sql; 2 queries; XX ms', - '- dummy-data/004.sql; 1 queries; XX ms', - '- structures/005.sql; 1 queries; XX ms', + '- structures/001.sql; 1 queries; XX s', + '- structures/002.sql; 1 queries; XX s', + '- basic-data/003.sql; 2 queries; XX s', + '- dummy-data/004.sql; 1 queries; XX s', + '- structures/005.sql; 1 queries; XX s', 'OK', ], $this->printer->lines); @@ -91,6 +91,24 @@ class FirstRunTest extends IntegrationTestCase Assert::type('DateTime', $migrations[2]->executedAt); Assert::same('basic-data', $migrations[2]->group); } + + public function testStatus() + { + $this->runner->run(Runner::MODE_STATUS); + Assert::same([ + 'Nextras Migrations', + 'STATUS', + 'No migrations has executed yet.', + '5 migrations need to be executed:', + '- structures/001.sql', + '- structures/002.sql', + '- basic-data/003.sql', + '- dummy-data/004.sql', + '- structures/005.sql', + 'OK', + ], $this->printer->lines); + + } public function testInit() diff --git a/tests/cases/integration/Runner.SecondRun.phpt b/tests/cases/integration/Runner.SecondRun.phpt index 029d1c0..be6d21f 100644 --- a/tests/cases/integration/Runner.SecondRun.phpt +++ b/tests/cases/integration/Runner.SecondRun.phpt @@ -27,11 +27,11 @@ class SecondRunTest extends IntegrationTestCase 'Nextras Migrations', 'RESET', '5 migrations need to be executed.', - '- structures/001.sql; 1 queries; XX ms', - '- structures/002.sql; 1 queries; XX ms', - '- basic-data/003.sql; 2 queries; XX ms', - '- dummy-data/004.sql; 1 queries; XX ms', - '- structures/005.sql; 1 queries; XX ms', + '- structures/001.sql; 1 queries; XX s', + '- structures/002.sql; 1 queries; XX s', + '- basic-data/003.sql; 2 queries; XX s', + '- dummy-data/004.sql; 1 queries; XX s', + '- structures/005.sql; 1 queries; XX s', 'OK', ], $this->printer->lines); @@ -49,8 +49,8 @@ class SecondRunTest extends IntegrationTestCase 'Nextras Migrations', 'CONTINUE', '2 migrations need to be executed.', - '- dummy-data/004.sql; 1 queries; XX ms', - '- structures/005.sql; 1 queries; XX ms', + '- dummy-data/004.sql; 1 queries; XX s', + '- structures/005.sql; 1 queries; XX s', 'OK', ], $this->printer->lines); @@ -95,6 +95,28 @@ class SecondRunTest extends IntegrationTestCase } } } + + public function testStatus() + { + $this->driver->loadFile($this->fixtureDir . '/3ok.sql'); + Assert::count(3, $this->driver->getAllMigrations()); + + $this->runner->run(Runner::MODE_STATUS); + Assert::same([ + 'Nextras Migrations', + 'STATUS', + 'Executed migrations:', + '- structures/001.sql OK', + '- structures/002.sql OK', + '- basic-data/003.sql OK', + ' ', + '2 migrations need to be executed:', + '- dummy-data/004.sql', + '- structures/005.sql', + 'OK', + ], $this->printer->lines); + + } } diff --git a/tests/cases/unit/BaseDriverTest.phpt b/tests/cases/unit/BaseDriverTest.phpt new file mode 100644 index 0000000..79a2540 --- /dev/null +++ b/tests/cases/unit/BaseDriverTest.phpt @@ -0,0 +1,86 @@ +shouldReceive('escapeIdentifier')->with('migrations')->andReturn('migrations'); + + $driver = Mockery::mock('Nextras\Migrations\Drivers\BaseDriver', array($dbal)); + $driver->shouldDeferMissing(); + + foreach ($expectedQueries as $expectedQuery) { + $dbal->shouldReceive('exec')->once()->ordered()->with($expectedQuery); + } + + $driver->loadFile(Tester\FileMock::create($content)); + + Mockery::close(); + Assert::true(TRUE); + } + + + protected function provideLoadFileData() + { + return [ + [ + 'SELECT 1', [ + 'SELECT 1', + ], + ], + [ + 'SELECT 1; ', [ + 'SELECT 1', + ], + ], + [ + 'SELECT 1; SELECT 2; SELECT 3; ', [ + 'SELECT 1', + ' SELECT 2', + ' SELECT 3', + ], + ], + [ + 'SELECT 1; SELECT 2; SELECT 3; ', [ + 'SELECT 1', + ' SELECT 2', + ' SELECT 3', + ], + ], + [ + implode("\n", [ + 'SELECT 1;', + 'DELIMITER //', + 'CREATE TRIGGER `users_bu` BEFORE UPDATE ON `users` FOR EACH ROW BEGIN SELECT 1; END; //', + 'DELIMITER ;', + 'SELECT 2;', + ]), + [ + 'SELECT 1', + "\nCREATE TRIGGER `users_bu` BEFORE UPDATE ON `users` FOR EACH ROW BEGIN SELECT 1; END; ", + "\nSELECT 2", + ] + ] + ]; + } +} + +$test = new BaseDriverTest(); +$test->run(); diff --git a/tests/inc/IntegrationTestCase.php b/tests/inc/IntegrationTestCase.php index 6fca3a9..b78d207 100644 --- a/tests/inc/IntegrationTestCase.php +++ b/tests/inc/IntegrationTestCase.php @@ -58,6 +58,8 @@ protected function setUp() $initDb(); $this->driver = $this->createDriver($options['driver'], $this->dbal); + $this->driver->setupConnection(); + $this->printer = $this->createPrinter(); $this->runner = new Runner($this->driver, $this->printer); diff --git a/tests/inc/TestPrinter.php b/tests/inc/TestPrinter.php index 7142372..b9760f7 100644 --- a/tests/inc/TestPrinter.php +++ b/tests/inc/TestPrinter.php @@ -23,7 +23,7 @@ public function __construct() protected function output($s, $color = NULL) { - $this->lines[] = preg_replace('#; \d+\.\d+ ms#', '; XX ms', $s); + $this->lines[] = preg_replace('#; \d+\.\d+ s#', '; XX s', $s); $this->out .= "$s\n"; } }