propelorm/Propel2

View on GitHub
src/Propel/Generator/Command/MigrationMigrateCommand.php

Summary

Maintainability
D
2 days
Test Coverage
<?php

/**
 * MIT License. This file is part of the Propel package.
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Propel\Generator\Command;

use Exception;
use Propel\Generator\Command\Executor\RollbackExecutor;
use Propel\Generator\Manager\MigrationManager;
use Propel\Generator\Util\SqlParser;
use Propel\Runtime\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author William Durand <william.durand1@gmail.com>
 */
class MigrationMigrateCommand extends AbstractCommand
{
    /**
     * @var string
     */
    protected const COMMAND_OPTION_MIGRATE_TO_VERSION = 'migrate-to-version';

    /**
     * @var string
     */
    protected const COMMAND_OPTION_MIGRATE_TO_VERSION_DESCRIPTION = 'Defines the version to migrate database to.';

    /**
     * @inheritDoc
     */
    protected function configure()
    {
        parent::configure();

        $this
            ->addOption('output-dir', null, InputOption::VALUE_REQUIRED, 'The output directory')
            ->addOption('migration-table', null, InputOption::VALUE_REQUIRED, 'Migration table name')
            ->addOption('connection', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Connection to use', [])
            ->addOption('fake', null, InputOption::VALUE_NONE, 'Does not touch the actual schema, but marks all migration as executed.')
            ->addOption('force', null, InputOption::VALUE_NONE, 'Continues with the migration even when errors occur.')
            ->addOption(static::COMMAND_OPTION_MIGRATE_TO_VERSION, null, InputOption::VALUE_REQUIRED, static::COMMAND_OPTION_MIGRATE_TO_VERSION_DESCRIPTION)
            ->setName('migration:migrate')
            ->setAliases(['migrate'])
            ->setDescription('Execute all pending migrations');
    }

    /**
     * @inheritDoc
     *
     * @throws \Propel\Runtime\Exception\RuntimeException
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $configOptions = [];

        if ($this->hasInputOption('output-dir', $input)) {
            $configOptions['propel']['paths']['migrationDir'] = $input->getOption('output-dir');
        }

        if ($this->hasInputOption('migration-table', $input)) {
            $configOptions['propel']['migrations']['tableName'] = $input->getOption('migration-table');
        }

        $generatorConfig = $this->getGeneratorConfig($configOptions, $input);

        $this->createDirectory($generatorConfig->getSection('paths')['migrationDir']);

        $manager = new MigrationManager();
        $manager->setGeneratorConfig($generatorConfig);

        $connections = [];
        $optionConnections = $input->getOption('connection');
        if (!$optionConnections) {
            $connections = $generatorConfig->getBuildConnections();
        } else {
            foreach ($optionConnections as $connection) {
                [$name, $dsn, $infos] = $this->parseConnection($connection);
                $connections[$name] = array_merge(['dsn' => $dsn], $infos);
            }
        }

        $manager->setConnections($connections);
        $manager->setMigrationTable($generatorConfig->getSection('migrations')['tableName']);
        $manager->setWorkingDirectory($generatorConfig->getSection('paths')['migrationDir']);

        $version = $input->getOption(static::COMMAND_OPTION_MIGRATE_TO_VERSION);
        if ($version && $manager->isDatabaseVersionApplied($version)) {
            return $this->executeRollbackToVersion($input, $output, $manager, $version);
        }

        if (!$manager->getFirstUpMigrationTimestamp()) {
            $output->writeln('All migrations were already executed - nothing to migrate.');

            return static::CODE_SUCCESS;
        }

        $timestamps = $manager->getNonExecutedMigrationTimestampsByVersion($version);
        if (count($timestamps) > 1) {
            $output->writeln(sprintf('%d migrations to execute', count($timestamps)));
        }

        foreach ($timestamps as $timestamp) {
            if ($input->getOption('fake')) {
                $output->writeln(
                    sprintf('Faking migration %s up', $manager->getMigrationClassName($timestamp)),
                );
            } else {
                $output->writeln(
                    sprintf('Executing migration %s up', $manager->getMigrationClassName($timestamp)),
                );
            }

            if (!$input->getOption('fake')) {
                $migration = $manager->getMigrationObject($timestamp);
                if (property_exists($migration, 'comment') && $migration->comment) {
                    $output->writeln(sprintf('<info>%s</info>', $migration->comment));
                }

                if ($migration->preUp($manager) === false) {
                    if ($input->getOption('force')) {
                        $output->writeln('<error>preUp() returned false. Continue migration.</error>');
                    } else {
                        $output->writeln('<error>preUp() returned false. Aborting migration.</error>');

                        return static::CODE_ERROR;
                    }
                }

                foreach ($migration->getUpSQL() as $datasource => $sql) {
                    $connection = $manager->getConnection($datasource);
                    if ($input->getOption('verbose')) {
                        $output->writeln(
                            sprintf(
                                'Connecting to database "%s" using DSN "%s"',
                                $datasource,
                                $connection['dsn'],
                            ),
                        );
                    }

                    $conn = $manager->getAdapterConnection($datasource);
                    $res = 0;
                    $statements = SqlParser::parseString($sql);

                    foreach ($statements as $statement) {
                        try {
                            if ($input->getOption('verbose')) {
                                $output->writeln(sprintf('Executing statement "%s"', $statement));
                            }
                            $conn->exec($statement);
                            $res++;
                        } catch (Exception $e) {
                            if ($input->getOption('force')) {
                                //continue, but print error message
                                $output->writeln(
                                    sprintf('<error>Failed to execute SQL "%s". Continue migration.</error>', $statement),
                                );
                            } else {
                                throw new RuntimeException(
                                    sprintf('<error>Failed to execute SQL "%s". Aborting migration.</error>', $statement),
                                    0,
                                    $e,
                                );
                            }
                        }
                    }

                    $output->writeln(
                        sprintf(
                            '%d of %d SQL statements executed successfully on datasource "%s"',
                            $res,
                            count($statements),
                            $datasource,
                        ),
                    );
                }
            }

            // migrations for datasources have passed - update the timestamp
            // for all datasources
            foreach ($manager->getConnections() as $datasource => $connection) {
                $manager->updateLatestMigrationTimestamp($datasource, $timestamp);
                if ($input->getOption('verbose')) {
                    $output->writeln(sprintf(
                        'Updated latest migration date to %d for datasource "%s"',
                        $timestamp,
                        $datasource,
                    ));
                }
            }

            if (!$input->getOption('fake')) {
                $migration->postUp($manager);
            }
        }

        $output->writeln('Migration complete. No further migration to execute.');

        return static::CODE_SUCCESS;
    }

    /**
     * @param \Symfony\Component\Console\Input\InputInterface $input
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     * @param \Propel\Generator\Manager\MigrationManager $migrationManager
     * @param int $version
     *
     * @return int
     */
    protected function executeRollbackToVersion(
        InputInterface $input,
        OutputInterface $output,
        MigrationManager $migrationManager,
        int $version
    ): int {
        $alreadyExecutedMigrations = $migrationManager->getAlreadyExecutedMigrationTimestampsByVersion($version);
        if ($alreadyExecutedMigrations === []) {
            $output->writeln(sprintf('Already at version %s.', $version));

            return static::CODE_SUCCESS;
        }

        $rollbackExecutor = new RollbackExecutor($input, $output, $migrationManager);

        while ($alreadyExecutedMigrations !== []) {
            $currentVersion = array_pop($alreadyExecutedMigrations);
            $previousVersion = count($alreadyExecutedMigrations) ? $alreadyExecutedMigrations[array_key_last($alreadyExecutedMigrations)] : null;

            if (!$rollbackExecutor->executeRollbackToPreviousVersion($currentVersion, $previousVersion)) {
                return static::CODE_ERROR;
            }
        }

        $output->writeln(sprintf('Successfully rollback to migration version %s.', $version));

        return static::CODE_SUCCESS;
    }
}