diff --git a/Command/BundleAwareTrait.php b/Command/BundleAwareTrait.php new file mode 100644 index 0000000..8025018 --- /dev/null +++ b/Command/BundleAwareTrait.php @@ -0,0 +1,157 @@ + + * + * @property ContainerInterface $container + */ +trait BundleAwareTrait +{ + + protected $configuration; + + /** + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * + * @return \AntiMattr\MongoDB\Migrations\Configuration\Interfaces\ConfigurationInterface + */ + + /** + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * + * @return \AntiMattr\MongoDB\Migrations\Configuration\Interfaces\ConfigurationInterface + */ + protected function getMigrationConfiguration(InputInterface $input, OutputInterface $output): ConfigurationInterface + { + if (null === $this->configuration) { + $configuration = parent::getMigrationConfiguration($input, $output); + + $bundleAlias = $input->getOption('bundle'); + + if (Configuration::DEFAULT_PREFIX !== $bundleAlias) { + $bundle = CommandHelper::getBundleByAlias($bundleAlias, $this->container); + if (null == $bundle) { + throw new \InvalidArgumentException("Bundle is not found for specified alias {$bundleAlias}"); + } else { + $configuration = $this->getConfigurationBuilder() + ->setConnection($configuration->getConnection()) + ->setOutputWriter($configuration->getOutputWriter()) + ->build(); + CommandHelper::configureConfiguration( + $this->container, + CommandHelper::getConfigParamsForBundle($this->container, $bundle), + $configuration + ); + } + } else { + CommandHelper::configureConfiguration( + $this->container, + CommandHelper::getConfigParams($this->container), + $configuration + ); + } + + $this->configuration = $configuration; + } + + return $this->configuration; + } + + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return ConfigurationInterface[] + */ + protected function getMigrationConfigurations(InputInterface $input, OutputInterface $output): array + { + $configs = [$this->getMigrationConfiguration($input, $output)]; + + $includeAllBundles = $input->getOption('include-bundles'); + $bundleAliasesList = $input->getOption('include-bundle'); + + if (!empty($includeAllBundles) && !empty($bundleAliasesList)) { + throw new \InvalidArgumentException( + 'Options "include-bundles" and "include-bundle" cannot be specified simultaneously.' + ); + } + + $outputWriter = new OutputWriter( + function ($message) use ($output) { + return $output->writeln($message); + } + ); + $databaseConnection = $this->getDatabaseConnection($input); + $matchedBundles = []; + + if ($includeAllBundles) { + /** @var array $registeredBundles */ + $registeredBundles = $this->container->getParameter('mongo_db_migrations.bundles'); + /** @var BundleInterface[] $appBundles */ + $appBundles = $this->container->get('kernel')->getBundles(); + + foreach ($appBundles as $bundle) { + $bundleExtension = $bundle->getContainerExtension(); + if (null !== $bundleExtension && !empty($registeredBundles[$bundleExtension->getAlias()])) { + $matchedBundles[] = $bundle; + } + } + } elseif (!empty($bundleAliasesList)) { + /** @var array $registeredBundles */ + $registeredBundles = $this->container->getParameter('mongo_db_migrations.bundles'); + /** @var BundleInterface[] $appBundles */ + $appBundles = $this->container->get('kernel')->getBundles(); + $bundleAliasesMap = \array_flip($bundleAliasesList); + foreach ($appBundles as $bundle) { + $bundleExtension = $bundle->getContainerExtension(); + if ( + null !== $bundleExtension && !empty($registeredBundles[$bundleExtension->getAlias()]) + && isset($bundleAliasesMap[$bundleExtension->getAlias()]) + ) { + $matchedBundles[] = $bundle; + } + } + } + + foreach ($matchedBundles as $bundle) { + $configs[] = $this->createConfiguration( + $databaseConnection, + $outputWriter, + CommandHelper::getConfigParamsForBundle($this->container, $bundle) + ); + } + + return $configs; + } + + protected function createConfiguration( + Connection $connection, + OutputWriter $outputWriter, + array $params + ): ConfigurationInterface { + $configuration = $this->getConfigurationBuilder() + ->setConnection($connection) + ->setOutputWriter($outputWriter) + ->build(); + CommandHelper::configureConfiguration($this->container, $params, $configuration); + + return $configuration; + } +} diff --git a/Command/CommandHelper.php b/Command/CommandHelper.php index 5ef27a8..022f6e0 100644 --- a/Command/CommandHelper.php +++ b/Command/CommandHelper.php @@ -1,4 +1,5 @@ @@ -25,23 +27,32 @@ final class CommandHelper /** * configureMigrations. * - * @param ContainerInterface $container - * @param Configuration $configuration + * @param ContainerInterface $container + * @param ConfigurationInterface $configuration */ - public static function configureMigrations(ContainerInterface $container, Configuration $configuration) + public static function configureMigrations(ContainerInterface $container, ConfigurationInterface $configuration) { - $dir = $container->getParameter('mongo_db_migrations.dir_name'); - if (!file_exists($dir)) { - mkdir($dir, 0777, true); + $params = self::getConfigParams($container); + self::configureConfiguration($container, $params, $configuration); + } + + public static function configureConfiguration( + ContainerInterface $container, + array $params, + ConfigurationInterface $configuration + ): void { + if (!\file_exists($params['dir_name'])) { + \mkdir($params['dir_name'], 0777, true); } - $configuration->setMigrationsCollectionName($container->getParameter('mongo_db_migrations.collection_name')); - $configuration->setMigrationsDatabaseName($container->getParameter('mongo_db_migrations.database_name')); - $configuration->setMigrationsDirectory($dir); - $configuration->setMigrationsNamespace($container->getParameter('mongo_db_migrations.namespace')); - $configuration->setName($container->getParameter('mongo_db_migrations.name')); - $configuration->registerMigrationsFromDirectory($dir); - $configuration->setMigrationsScriptDirectory($container->getParameter('mongo_db_migrations.script_dir_name')); + $configuration->setPrefix($params['prefix']); + $configuration->setMigrationsCollectionName($params['collection_name']); + $configuration->setMigrationsDatabaseName($params['database_name']); + $configuration->setMigrationsDirectory($params['dir_name']); + $configuration->setMigrationsNamespace($params['namespace']); + $configuration->setName($params['name']); + $configuration->registerMigrationsFromDirectory($params['dir_name']); + $configuration->setMigrationsScriptDirectory($params['script_dir_name']); self::injectContainerToMigrations($container, $configuration->getMigrations()); } @@ -62,6 +73,63 @@ public static function setApplicationDocumentManager(Application $application, $ $helperSet->set(new DocumentManagerHelper($dm), 'dm'); } + public static function getConfigParams(ContainerInterface $container): array + { + return [ + 'collection_name' => $container->getParameter('mongo_db_migrations.collection_name'), + 'database_name' => $container->getParameter('mongo_db_migrations.database_name'), + 'script_dir_name' => $container->getParameter('mongo_db_migrations.script_dir_name'), + 'name' => $container->getParameter('mongo_db_migrations.name'), + 'namespace' => $container->getParameter('mongo_db_migrations.namespace'), + 'dir_name' => $container->getParameter('mongo_db_migrations.dir_name'), + 'prefix' => \AntiMattr\MongoDB\Migrations\Configuration\Configuration::DEFAULT_PREFIX, + ]; + } + + public static function getConfigParamsForBundle( + ContainerInterface $container, + BundleInterface $bundle + ): array { + if ($bundle->getContainerExtension() == null) { + throw new \InvalidArgumentException( + "Bundle with name {$bundle->getName()} do not have bundle extension. Bundle alias cannot be defined." + ); + } + $bundleAlias = $bundle->getContainerExtension()->getAlias(); + $bundleConfigs = $container->getParameter('mongo_db_migrations.bundles'); + if (empty($bundleConfigs[$bundleAlias])) { + throw new \RuntimeException("Bundle with alias {$bundleAlias} has no registered migration configs"); + } + + $bundleConfig = $bundleConfigs[$bundleAlias]; + + return [ + 'collection_name' => $container->getParameter('mongo_db_migrations.collection_name'), + 'database_name' => $container->getParameter('mongo_db_migrations.database_name'), + 'script_dir_name' => $container->getParameter('mongo_db_migrations.script_dir_name'), + 'namespace' => $bundle->getNamespace() . '\\' . $bundleConfig['namespace'], + 'dir_name' => $bundle->getPath() . '/' . $bundleConfig['dir_name'], + 'name' => $bundleConfig['name'], + 'prefix' => $bundleAlias, + ]; + } + + public static function getBundleByAlias(string $bundleAlias, ContainerInterface $container): ?BundleInterface + { + /** @var BundleInterface[] $bundles */ + $bundles = $container->get('kernel')->getBundles(); + $targetBundle = null; + foreach ($bundles as $bundle) { + $containerExtension = $bundle->getContainerExtension(); + if (null !== $containerExtension && $containerExtension->getAlias() === $bundleAlias) { + $targetBundle = $bundle; + break; + } + } + + return $targetBundle; + } + /** * injectContainerToMigrations. * diff --git a/Command/MigrationsBCFixCommand.php b/Command/MigrationsBCFixCommand.php new file mode 100644 index 0000000..227bbbd --- /dev/null +++ b/Command/MigrationsBCFixCommand.php @@ -0,0 +1,62 @@ + + */ +class MigrationsBCFixCommand extends BCFixCommand +{ + + protected $container; + + public function __construct(?string $name = null, ContainerInterface $container) + { + parent::__construct($name); + $this->container = $container; + } + + + protected function configure(): void + { + parent::configure(); + + $this->setName('mongodb:migrations:bc-fix'); + $this->addOption( + 'dm', + null, + InputOption::VALUE_OPTIONAL, + 'The document manager to use for this command.', + 'default_document_manager' + ); + } + + public function execute(InputInterface $input, OutputInterface $output): void + { + CommandHelper::setApplicationDocumentManager($this->getApplication(), $input->getOption('dm')); + $configuration = $this->getMigrationConfiguration($input, $output); + CommandHelper::configureConfiguration( + $this->container, + CommandHelper::getConfigParams($this->container), + $configuration + ); + + parent::execute($input, $output); + } +} diff --git a/Command/MigrationsExecuteCommand.php b/Command/MigrationsExecuteCommand.php index 106ba19..42701f5 100644 --- a/Command/MigrationsExecuteCommand.php +++ b/Command/MigrationsExecuteCommand.php @@ -1,4 +1,5 @@ */ class MigrationsExecuteCommand extends ExecuteCommand { - protected function configure() + use BundleAwareTrait; + + protected $container; + + public function __construct(?string $name = null, ContainerInterface $container) + { + parent::__construct($name); + $this->container = $container; + } + + protected function configure(): void { parent::configure(); - $this - ->setName('mongodb:migrations:execute') - ->addOption('dm', null, InputOption::VALUE_OPTIONAL, 'The document manager to use for this command.', 'default_document_manager') + $this->setName('mongodb:migrations:execute'); + $this->addOption( + 'bundle', + null, + InputOption::VALUE_OPTIONAL, + 'Alias of bundle for which action is performed', + Configuration::DEFAULT_PREFIX + ); + $this->addOption( + 'dm', + null, + InputOption::VALUE_OPTIONAL, + 'The document manager to use for this command.', + 'default_document_manager' + ); ; } @@ -35,9 +60,6 @@ public function execute(InputInterface $input, OutputInterface $output) { CommandHelper::setApplicationDocumentManager($this->getApplication(), $input->getOption('dm')); - $configuration = $this->getMigrationConfiguration($input, $output); - CommandHelper::configureMigrations($this->getApplication()->getKernel()->getContainer(), $configuration); - parent::execute($input, $output); } } diff --git a/Command/MigrationsGenerateCommand.php b/Command/MigrationsGenerateCommand.php index 8d7bf78..0620882 100644 --- a/Command/MigrationsGenerateCommand.php +++ b/Command/MigrationsGenerateCommand.php @@ -1,4 +1,5 @@ */ class MigrationsGenerateCommand extends GenerateCommand { - protected function configure() + use BundleAwareTrait; + + protected $container; + + public function __construct(?string $name = null, ContainerInterface $container) + { + parent::__construct($name); + $this->container = $container; + } + + protected function configure(): void { parent::configure(); - $this - ->setName('mongodb:migrations:generate') - ->addOption('dm', null, InputOption::VALUE_OPTIONAL, 'The document manager to use for this command.', 'default_document_manager') - ; + $this->setName('mongodb:migrations:generate'); + $this->addOption( + 'bundle', + null, + InputOption::VALUE_OPTIONAL, + 'Alias of bundle for which migration will be generated.', + Configuration::DEFAULT_PREFIX + ); + $this->addOption( + 'dm', + null, + InputOption::VALUE_OPTIONAL, + 'The document manager to use for this command.', + 'default_document_manager' + ); } - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): void { CommandHelper::setApplicationDocumentManager($this->getApplication(), $input->getOption('dm')); - $configuration = $this->getMigrationConfiguration($input, $output); - CommandHelper::configureMigrations($this->getApplication()->getKernel()->getContainer(), $configuration); - parent::execute($input, $output); } } diff --git a/Command/MigrationsMigrateCommand.php b/Command/MigrationsMigrateCommand.php index 66f57db..47f324c 100644 --- a/Command/MigrationsMigrateCommand.php +++ b/Command/MigrationsMigrateCommand.php @@ -11,33 +11,132 @@ namespace AntiMattr\Bundle\MongoDBMigrationsBundle\Command; +use AntiMattr\MongoDB\Migrations\Configuration\Configuration; use AntiMattr\MongoDB\Migrations\Tools\Console\Command\MigrateCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * @author Matthew Fitzgerald */ class MigrationsMigrateCommand extends MigrateCommand { - protected function configure() + use BundleAwareTrait; + + protected $container; + + + public function __construct(?string $name = null, ContainerInterface $container) + { + parent::__construct($name); + $this->container = $container; + } + + protected function configure(): void { parent::configure(); - $this - ->setName('mongodb:migrations:migrate') - ->addOption('dm', null, InputOption::VALUE_OPTIONAL, 'The document manager to use for this command.', 'default_document_manager') - ; + $this->setName('mongodb:migrations:migrate'); + $this->addOption( + 'include-bundle', + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Alias of bundle which migrations should be applied.' + ); + $this->addOption( + 'include-bundles', + null, + InputOption::VALUE_NONE, + 'Indicate that all migrations should be applied.' + ); + $this->addOption( + 'bundle', + null, + InputOption::VALUE_OPTIONAL, + 'Alias of bundle for which migration will be generated.', + Configuration::DEFAULT_PREFIX + ); + $this->addOption( + 'dm', + null, + InputOption::VALUE_OPTIONAL, + 'The document manager to use for this command.', + 'default_document_manager' + ); } public function execute(InputInterface $input, OutputInterface $output) { CommandHelper::setApplicationDocumentManager($this->getApplication(), $input->getOption('dm')); - $configuration = $this->getMigrationConfiguration($input, $output); - CommandHelper::configureMigrations($this->getApplication()->getKernel()->getContainer(), $configuration); + $version = $input->getArgument('version'); + $configurations = $this->getMigrationConfigurations($input, $output); + + $isInteractive = $input->isInteractive(); + + + // warn the user if no dry run and interaction is on + if ($isInteractive) { + $question = new ConfirmationQuestion( + 'WARNING! You are about to execute a database migration that could result in data lost. Are you sure you wish to continue? (y/[n]) ', + false + ); + + $confirmation = $this->getHelper('question')->ask($input, $output, $question); + + if (!$confirmation) { + $output->writeln('Migration cancelled!'); + + return 1; + } + } + + foreach ($configurations as $configuration) { + + $migration = $this->createMigration($configuration); + $this->outputHeader($configuration, $output); + + $executedVersions = $configuration->getMigratedVersions(); + $availableVersions = $configuration->getAvailableVersions(); + $executedUnavailableVersions = array_diff($executedVersions, $availableVersions); + + if (!empty($executedUnavailableVersions)) { + $output->writeln(sprintf('WARNING! You have %s previously executed migrations in the database that are not registered migrations.', + count($executedUnavailableVersions))); + foreach ($executedUnavailableVersions as $executedUnavailableVersion) { + $output->writeln( + sprintf( + ' >> %s (%s)', + Configuration::formatVersion($executedUnavailableVersion), + $executedUnavailableVersion + ) + ); + } + + if ($isInteractive) { + $question = new ConfirmationQuestion( + 'Are you sure you wish to continue? (y/[n]) ', + false + ); + + $confirmation = $this + ->getHelper('question') + ->ask($input, $output, $question); + + if (!$confirmation) { + $output->writeln('Migration cancelled!'); + + return 1; + } + } + } + + $migration->migrate($version); + } - parent::execute($input, $output); + return 0; } } diff --git a/Command/MigrationsStatusCommand.php b/Command/MigrationsStatusCommand.php index 67d69fc..f3e5c14 100644 --- a/Command/MigrationsStatusCommand.php +++ b/Command/MigrationsStatusCommand.php @@ -1,4 +1,5 @@ */ class MigrationsStatusCommand extends StatusCommand { - protected function configure() + use BundleAwareTrait; + + protected $container; + + public function __construct(?string $name = null, ContainerInterface $container) + { + parent::__construct($name); + $this->container = $container; + } + + protected function configure(): void { parent::configure(); - $this - ->setName('mongodb:migrations:status') - ->addOption('dm', null, InputOption::VALUE_OPTIONAL, 'The document manager to use for this command.', 'default_document_manager') - ; + $this->setName('mongodb:migrations:status'); + $this->addOption( + 'include-bundle', + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Alias of bundle which migrations should be applied.' + ); + $this->addOption( + 'include-bundles', + null, + InputOption::VALUE_NONE, + 'Indicate that all migrations should be applied.' + ); + $this->addOption( + 'bundle', + null, + InputOption::VALUE_OPTIONAL, + 'Alias of bundle for which migration will be generated.', + Configuration::DEFAULT_PREFIX + ); + $this->addOption( + 'dm', + null, + InputOption::VALUE_OPTIONAL, + 'The document manager to use for this command.', + 'default_document_manager' + ); } public function execute(InputInterface $input, OutputInterface $output) { CommandHelper::setApplicationDocumentManager($this->getApplication(), $input->getOption('dm')); - $configuration = $this->getMigrationConfiguration($input, $output); - CommandHelper::configureMigrations($this->getApplication()->getKernel()->getContainer(), $configuration); + $configurations = $this->getMigrationConfigurations($input, $output); + + foreach ($configurations as $configuration) { + $this->configuration = $configuration; + parent::execute($input, $output); + } - parent::execute($input, $output); } } diff --git a/Command/MigrationsVersionCommand.php b/Command/MigrationsVersionCommand.php index ff87d0a..c63a7ab 100644 --- a/Command/MigrationsVersionCommand.php +++ b/Command/MigrationsVersionCommand.php @@ -1,4 +1,5 @@ */ class MigrationsVersionCommand extends VersionCommand { - protected function configure() + use BundleAwareTrait; + + protected $container; + + public function __construct(?string $name = null, ContainerInterface $container) + { + parent::__construct($name); + $this->container = $container; + } + + + protected function configure(): void { parent::configure(); - $this - ->setName('mongodb:migrations:version') - ->addOption('dm', null, InputOption::VALUE_OPTIONAL, 'The document manager to use for this command.', 'default_document_manager') - ; + $this->setName('mongodb:migrations:version'); + $this->addOption( + 'bundle', + null, + InputOption::VALUE_OPTIONAL, + 'Alias of bundle for which action is performed', + Configuration::DEFAULT_PREFIX + ); + $this->addOption( + 'dm', + null, + InputOption::VALUE_OPTIONAL, + 'The document manager to use for this command.', + 'default_document_manager' + ); } public function execute(InputInterface $input, OutputInterface $output) { CommandHelper::setApplicationDocumentManager($this->getApplication(), $input->getOption('dm')); - $configuration = $this->getMigrationConfiguration($input, $output); - CommandHelper::configureMigrations($this->getApplication()->getKernel()->getContainer(), $configuration); - parent::execute($input, $output); } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 497dcd6..9a69f40 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -37,6 +37,15 @@ public function getConfigTreeBuilder() ->scalarNode('name')->defaultValue('Application MongoDB Migrations')->end() ->scalarNode('namespace')->defaultValue('Application\MongoDBMigrations')->cannotBeEmpty()->end() ->scalarNode('script_dir_name')->end() + ->arrayNode('bundles') + ->canBeUnset() + ->useAttributeAsKey('key') + ->prototype('array') + ->children() + ->scalarNode('defaults')->cannotBeEmpty()->defaultFalse()->end() + ->scalarNode('dir_name')->defaultValue('MongoDBMigrations')->end() + ->scalarNode('name')->defaultValue('Bundle MongoDB Migrations')->end() + ->scalarNode('namespace')->defaultValue('MongoDBMigrations')->end() ->end() ; diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 6055cf4..fd6bd61 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -4,6 +4,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + + + + diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php index 78f48d4..b3d9bbc 100644 --- a/Tests/bootstrap.php +++ b/Tests/bootstrap.php @@ -1,11 +1 @@