diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 8fe48c1d..3e399791 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -132,6 +132,11 @@ $executedVersion + + + ConfigurationTrait + + ConfigurationTrait diff --git a/src/Command/MarkMigratedCommand.php b/src/Command/MarkMigratedCommand.php index 716c7b32..4567b315 100644 --- a/src/Command/MarkMigratedCommand.php +++ b/src/Command/MarkMigratedCommand.php @@ -135,7 +135,8 @@ public function execute(Arguments $args, ConsoleIo $io): ?int return self::CODE_ERROR; } - $manager->markVersionsAsMigrated($path, $versions, $io); + $output = $manager->markVersionsAsMigrated($path, $versions); + array_map(fn ($line) => $io->out($line), $output); return self::CODE_SUCCESS; } diff --git a/src/Db/Adapter/PdoAdapter.php b/src/Db/Adapter/PdoAdapter.php index cc46286e..0cf25416 100644 --- a/src/Db/Adapter/PdoAdapter.php +++ b/src/Db/Adapter/PdoAdapter.php @@ -119,7 +119,6 @@ public function setOptions(array $options): AdapterInterface { parent::setOptions($options); - // TODO: Consider renaming this class to ConnectionAdapter if (isset($options['connection']) && $options['connection'] instanceof Connection) { $this->setConnection($options['connection']); } diff --git a/src/Migration/BuiltinBackend.php b/src/Migration/BuiltinBackend.php new file mode 100644 index 00000000..470621bc --- /dev/null +++ b/src/Migration/BuiltinBackend.php @@ -0,0 +1,251 @@ + + */ + protected array $default = []; + + /** + * Current command being run. + * Useful if some logic needs to be applied in the ConfigurationTrait depending + * on the command + * + * @var string + */ + protected string $command; + + /** + * Stub input to feed the manager class since we might not have an input ready when we get the Manager using + * the `getManager()` method + * + * @var \Symfony\Component\Console\Input\ArrayInput + */ + protected ArrayInput $stubInput; + + /** + * Constructor + * + * @param array $default Default option to be used when calling a method. + * Available options are : + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + */ + public function __construct(array $default = []) + { + $this->output = new NullOutput(); + $this->stubInput = new ArrayInput([]); + + if ($default) { + $this->default = $default; + } + } + + /** + * Returns the status of each migrations based on the options passed + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `format` Format to output the response. Can be 'json' + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * @return array The migrations list and their statuses + */ + public function status(array $options = []): array + { + $manager = $this->getManager($options); + + return $manager->printStatus($options['format'] ?? null); + } + + /** + * Migrates available migrations + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `target` The version number to migrate to. If not provided, will migrate + * everything it can + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * - `date` The date to migrate to + * @return bool Success + */ + public function migrate(array $options = []): bool + { + $manager = $this->getManager($options); + + if (!empty($options['date'])) { + $date = new DateTime($options['date']); + + $manager->migrateToDateTime($date); + + return true; + } + + $manager->migrate($options['target'] ?? null); + + return true; + } + + /** + * Rollbacks migrations + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `target` The version number to migrate to. If not provided, will only migrate + * the last migrations registered in the phinx log + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * - `date` The date to rollback to + * @return bool Success + */ + public function rollback(array $options = []): bool + { + $manager = $this->getManager($options); + + if (!empty($options['date'])) { + $date = new DateTime($options['date']); + + $manager->rollbackToDateTime($date); + + return true; + } + + $manager->rollback($options['target'] ?? null); + + return true; + } + + /** + * Marks a migration as migrated + * + * @param int|string|null $version The version number of the migration to mark as migrated + * @param array $options Options to pass to the command + * Available options are : + * + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * @return bool Success + */ + public function markMigrated(int|string|null $version = null, array $options = []): bool + { + if ( + isset($options['target']) && + isset($options['exclude']) && + isset($options['only']) + ) { + $exceptionMessage = 'You should use `exclude` OR `only` (not both) along with a `target` argument'; + throw new InvalidArgumentException($exceptionMessage); + } + $args = new Arguments([(string)$version], $options, ['version']); + + $manager = $this->getManager($options); + $config = $manager->getConfig(); + $path = $config->getMigrationPaths()[0]; + + $versions = $manager->getVersionsToMark($args); + $manager->markVersionsAsMigrated($path, $versions); + + return true; + } + + /** + * Seed the database using a seed file + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * - `seed` The seed file to use + * @return bool Success + */ + public function seed(array $options = []): bool + { + $seed = $options['seed'] ?? null; + $manager = $this->getManager($options); + $manager->seed($seed); + + return true; + } + + /** + * Returns an instance of Manager + * + * @param array $options The options for manager creation + * @return \Migrations\Migration\Manager Instance of Manager + */ + public function getManager(array $options): Manager + { + $options += $this->default; + + $factory = new ManagerFactory([ + 'plugin' => $options['plugin'] ?? null, + 'source' => $options['source'] ?? null, + 'connection' => $options['connection'] ?? 'default', + ]); + $io = new ConsoleIo( + new StubConsoleOutput(), + new StubConsoleOutput(), + new StubConsoleInput([]), + ); + + return $factory->createManager($io); + } +} diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index 200efe85..475620cb 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -22,7 +22,9 @@ use Phinx\Util\Util; use Psr\Container\ContainerInterface; use RuntimeException; -use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; class Manager { @@ -161,7 +163,6 @@ protected function printMissingVersion(array $version, int $maxNameLength): void */ public function migrateToDateTime(DateTime $dateTime, bool $fake = false): void { - // TODO remove the environment parameter. There is only one environment with builtin /** @var array $versions */ $versions = array_keys($this->getMigrations()); $dateString = $dateTime->format('Ymdhis'); @@ -301,7 +302,6 @@ public function getVersionsToMark(Arguments $args): array $migrations = $this->getMigrations(); $versions = array_keys($migrations); - // TODO use console arguments $versionArg = $args->getArgument('version'); $targetArg = $args->getOption('target'); $hasAllVersion = in_array($versionArg, ['all', '*'], true); @@ -336,46 +336,44 @@ public function getVersionsToMark(Arguments $args): array * * @param string $path Path where to look for migrations * @param array $versions Versions which should be marked - * @param \Cake\Console\ConsoleIo $io ConsoleIo to write output too - * @return void + * @return list Output from the operation */ - public function markVersionsAsMigrated(string $path, array $versions, ConsoleIo $io): void + public function markVersionsAsMigrated(string $path, array $versions): array { $adapter = $this->getEnvironment()->getAdapter(); + $out = []; if (!$versions) { - $io->out('No migrations were found. Nothing to mark as migrated.'); + $out[] = 'No migrations were found. Nothing to mark as migrated.'; - return; + return $out; } $adapter->beginTransaction(); foreach ($versions as $version) { if ($this->isMigrated($version)) { - $io->out(sprintf('Skipping migration `%s` (already migrated).', $version)); + $out[] = sprintf('Skipping migration `%s` (already migrated).', $version); continue; } try { $this->markMigrated($version, $path); - $io->out( - sprintf('Migration `%s` successfully marked migrated !', $version) - ); + $out[] = sprintf('Migration `%s` successfully marked migrated !', $version); } catch (Exception $e) { $adapter->rollbackTransaction(); - $io->out( - sprintf( - 'An error occurred while marking migration `%s` as migrated : %s', - $version, - $e->getMessage() - ) + $out[] = sprintf( + 'An error occurred while marking migration `%s` as migrated : %s', + $version, + $e->getMessage() ); - $io->out('All marked migrations during this process were unmarked.'); + $out[] = 'All marked migrations during this process were unmarked.'; - return; + return $out; } } $adapter->commitTransaction(); + + return $out; } /** @@ -851,7 +849,12 @@ function ($phpFile) { $io->verbose("Constructing $class."); - $input = new ArgvInput(); + $config = $this->getConfig(); + $input = new ArrayInput([ + '--plugin' => $config['plugin'] ?? null, + '--source' => $config['source'] ?? null, + '--connection' => $config->getConnection(), + ]); $output = new OutputAdapter($io); // instantiate it @@ -960,7 +963,17 @@ public function getSeeds(): array /** @var \Phinx\Seed\SeedInterface[] $seeds */ $seeds = []; - $input = new ArgvInput(); + $config = $this->getConfig(); + $optionDef = new InputDefinition([ + new InputOption('plugin', mode: InputOption::VALUE_OPTIONAL, default: ''), + new InputOption('connection', mode: InputOption::VALUE_OPTIONAL, default: ''), + new InputOption('source', mode: InputOption::VALUE_OPTIONAL, default: ''), + ]); + $input = new ArrayInput([ + '--plugin' => $config['plugin'] ?? null, + '--source' => $config['source'] ?? null, + '--connection' => $config->getConnection(), + ], $optionDef); $output = new OutputAdapter($this->io); foreach ($phpFiles as $filePath) { @@ -1012,7 +1025,6 @@ public function getSeeds(): array } foreach ($this->seeds as $instance) { - // TODO fix this to not use input if (isset($input) && $instance instanceof AbstractSeed) { $instance->setInput($input); } diff --git a/src/Migration/ManagerFactory.php b/src/Migration/ManagerFactory.php index 2ccaa6bb..0d996e9b 100644 --- a/src/Migration/ManagerFactory.php +++ b/src/Migration/ManagerFactory.php @@ -92,8 +92,6 @@ public function createConfig(): ConfigInterface $templatePath = dirname(__DIR__) . DS . 'templates' . DS; $connectionName = (string)$this->getOption('connection'); - // TODO this all needs to go away. But first Environment and Manager need to work - // with Cake's ConnectionManager. $connectionConfig = ConnectionManager::getConfig($connectionName); if (!$connectionConfig) { throw new RuntimeException("Could not find connection `{$connectionName}`"); @@ -120,6 +118,8 @@ public function createConfig(): ConfigInterface ], 'migration_base_class' => 'Migrations\AbstractMigration', 'environment' => $adapterConfig, + 'plugin' => $plugin, + 'source' => (string)$this->getOption('source'), // TODO do we want to support the DI container in migrations? ]; diff --git a/src/Migration/PhinxBackend.php b/src/Migration/PhinxBackend.php new file mode 100644 index 00000000..2c41fd55 --- /dev/null +++ b/src/Migration/PhinxBackend.php @@ -0,0 +1,455 @@ + + */ + protected array $default = []; + + /** + * Current command being run. + * Useful if some logic needs to be applied in the ConfigurationTrait depending + * on the command + * + * @var string + */ + protected string $command; + + /** + * Stub input to feed the manager class since we might not have an input ready when we get the Manager using + * the `getManager()` method + * + * @var \Symfony\Component\Console\Input\ArrayInput + */ + protected ArrayInput $stubInput; + + /** + * Constructor + * + * @param array $default Default option to be used when calling a method. + * Available options are : + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + */ + public function __construct(array $default = []) + { + $this->output = new NullOutput(); + $this->stubInput = new ArrayInput([]); + + if ($default) { + $this->default = $default; + } + } + + /** + * Sets the command + * + * @param string $command Command name to store. + * @return $this + */ + public function setCommand(string $command) + { + $this->command = $command; + + return $this; + } + + /** + * Sets the input object that should be used for the command class. This object + * is used to inspect the extra options that are needed for CakePHP apps. + * + * @param \Symfony\Component\Console\Input\InputInterface $input the input object + * @return void + */ + public function setInput(InputInterface $input): void + { + $this->input = $input; + } + + /** + * Gets the command + * + * @return string Command name + */ + public function getCommand(): string + { + return $this->command; + } + + /** + * Returns the status of each migrations based on the options passed + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `format` Format to output the response. Can be 'json' + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * @return array The migrations list and their statuses + */ + public function status(array $options = []): array + { + // TODO This class could become an interface that chooses between a phinx and builtin + // implementation. Having two implementations would be easier to cleanup + // than having all the logic in one class with branching + $input = $this->getInput('Status', [], $options); + $params = ['default', $input->getOption('format')]; + + return $this->run('printStatus', $params, $input); + } + + /** + * Migrates available migrations + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `target` The version number to migrate to. If not provided, will migrate + * everything it can + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * - `date` The date to migrate to + * @return bool Success + */ + public function migrate(array $options = []): bool + { + $this->setCommand('migrate'); + $input = $this->getInput('Migrate', [], $options); + $method = 'migrate'; + $params = ['default', $input->getOption('target')]; + + if ($input->getOption('date')) { + $method = 'migrateToDateTime'; + $params[1] = new DateTime($input->getOption('date')); + } + + $this->run($method, $params, $input); + + return true; + } + + /** + * Rollbacks migrations + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `target` The version number to migrate to. If not provided, will only migrate + * the last migrations registered in the phinx log + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * - `date` The date to rollback to + * @return bool Success + */ + public function rollback(array $options = []): bool + { + $this->setCommand('rollback'); + $input = $this->getInput('Rollback', [], $options); + $method = 'rollback'; + $params = ['default', $input->getOption('target')]; + + if ($input->getOption('date')) { + $method = 'rollbackToDateTime'; + $params[1] = new DateTime($input->getOption('date')); + } + + $this->run($method, $params, $input); + + return true; + } + + /** + * Marks a migration as migrated + * + * @param int|string|null $version The version number of the migration to mark as migrated + * @param array $options Options to pass to the command + * Available options are : + * + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * @return bool Success + */ + public function markMigrated(int|string|null $version = null, array $options = []): bool + { + $this->setCommand('mark_migrated'); + + if ( + isset($options['target']) && + isset($options['exclude']) && + isset($options['only']) + ) { + $exceptionMessage = 'You should use `exclude` OR `only` (not both) along with a `target` argument'; + throw new InvalidArgumentException($exceptionMessage); + } + + $input = $this->getInput('MarkMigrated', ['version' => $version], $options); + $this->setInput($input); + + // This will need to vary based on the config option. + $migrationPaths = $this->getConfig()->getMigrationPaths(); + $config = $this->getConfig(true); + $params = [ + array_pop($migrationPaths), + $this->getManager($config)->getVersionsToMark($input), + $this->output, + ]; + + $this->run('markVersionsAsMigrated', $params, $input); + + return true; + } + + /** + * Seed the database using a seed file + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * - `seed` The seed file to use + * @return bool Success + */ + public function seed(array $options = []): bool + { + $this->setCommand('seed'); + $input = $this->getInput('Seed', [], $options); + + $seed = $input->getOption('seed'); + if (!$seed) { + $seed = null; + } + + $params = ['default', $seed]; + $this->run('seed', $params, $input); + + return true; + } + + /** + * Runs the method needed to execute and return + * + * @param string $method Manager method to call + * @param array $params Manager params to pass + * @param \Symfony\Component\Console\Input\InputInterface $input InputInterface needed for the + * Manager to properly run + * @return mixed The result of the CakeManager::$method() call + */ + protected function run(string $method, array $params, InputInterface $input): mixed + { + // This will need to vary based on the backend configuration + if ($this->configuration instanceof Config) { + $migrationPaths = $this->getConfig()->getMigrationPaths(); + $migrationPath = array_pop($migrationPaths); + $seedPaths = $this->getConfig()->getSeedPaths(); + $seedPath = array_pop($seedPaths); + } + + $pdo = null; + if ($this->manager instanceof Manager) { + $pdo = $this->manager->getEnvironment('default') + ->getAdapter() + ->getConnection(); + } + + $this->setInput($input); + $newConfig = $this->getConfig(true); + $manager = $this->getManager($newConfig); + $manager->setInput($input); + + // Why is this being done? Is this something we can eliminate in the new code path? + if ($pdo !== null) { + /** @var \Phinx\Db\Adapter\PdoAdapter|\Migrations\CakeAdapter $adapter */ + /** @psalm-suppress PossiblyNullReference */ + $adapter = $this->manager->getEnvironment('default')->getAdapter(); + while ($adapter instanceof WrapperInterface) { + /** @var \Phinx\Db\Adapter\PdoAdapter|\Migrations\CakeAdapter $adapter */ + $adapter = $adapter->getAdapter(); + } + $adapter->setConnection($pdo); + } + + $newMigrationPaths = $newConfig->getMigrationPaths(); + if (isset($migrationPath) && array_pop($newMigrationPaths) !== $migrationPath) { + $manager->resetMigrations(); + } + $newSeedPaths = $newConfig->getSeedPaths(); + if (isset($seedPath) && array_pop($newSeedPaths) !== $seedPath) { + $manager->resetSeeds(); + } + + /** @var callable $callable */ + $callable = [$manager, $method]; + + return call_user_func_array($callable, $params); + } + + /** + * Returns an instance of CakeManager + * + * @param \Phinx\Config\ConfigInterface|null $config ConfigInterface the Manager needs to run + * @return \Migrations\CakeManager Instance of CakeManager + */ + public function getManager(?ConfigInterface $config = null): CakeManager + { + if (!($this->manager instanceof CakeManager)) { + if (!($config instanceof ConfigInterface)) { + throw new RuntimeException( + 'You need to pass a ConfigInterface object for your first getManager() call' + ); + } + + $input = $this->input ?: $this->stubInput; + $this->manager = new CakeManager($config, $input, $this->output); + } elseif ($config !== null) { + $defaultEnvironment = $config->getEnvironment('default'); + try { + $environment = $this->manager->getEnvironment('default'); + $oldConfig = $environment->getOptions(); + unset($oldConfig['connection']); + if ($oldConfig === $defaultEnvironment) { + $defaultEnvironment['connection'] = $environment + ->getAdapter() + ->getConnection(); + } + } catch (InvalidArgumentException $e) { + } + $config['environments'] = ['default' => $defaultEnvironment]; + $this->manager->setEnvironments([]); + $this->manager->setConfig($config); + } + + $this->setAdapter(); + + return $this->manager; + } + + /** + * Sets the adapter the manager is going to need to operate on the DB + * This will make sure the adapter instance is a \Migrations\CakeAdapter instance + * + * @return void + */ + public function setAdapter(): void + { + if ($this->input === null) { + return; + } + + /** @var string $connectionName */ + $connectionName = $this->input()->getOption('connection') ?: 'default'; + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get($connectionName); + + /** @psalm-suppress PossiblyNullReference */ + $env = $this->manager->getEnvironment('default'); + $adapter = $env->getAdapter(); + if (!$adapter instanceof CakeAdapter) { + $env->setAdapter(new CakeAdapter($adapter, $connection)); + } + } + + /** + * Get the input needed for each commands to be run + * + * @param string $command Command name for which we need the InputInterface + * @param array $arguments Simple key/values array representing the command arguments + * to pass to the InputInterface + * @param array $options Simple key/values array representing the command options + * to pass to the InputInterface + * @return \Symfony\Component\Console\Input\InputInterface InputInterface needed for the + * Manager to properly run + */ + public function getInput(string $command, array $arguments, array $options): InputInterface + { + $className = 'Migrations\Command\Phinx\\' . $command; + $options = $arguments + $this->prepareOptions($options); + /** @var \Symfony\Component\Console\Command\Command $command */ + $command = new $className(); + $definition = $command->getDefinition(); + + return new ArrayInput($options, $definition); + } + + /** + * Prepares the option to pass on to the InputInterface + * + * @param array $options Simple key-values array to pass to the InputInterface + * @return array Prepared $options + */ + protected function prepareOptions(array $options = []): array + { + $options += $this->default; + if (!$options) { + return $options; + } + + foreach ($options as $name => $value) { + $options['--' . $name] = $value; + unset($options[$name]); + } + + return $options; + } +} diff --git a/src/Migrations.php b/src/Migrations.php index fb486fff..1ed553b9 100644 --- a/src/Migrations.php +++ b/src/Migrations.php @@ -13,13 +13,12 @@ */ namespace Migrations; +use Cake\Core\Configure; use Cake\Datasource\ConnectionManager; -use DateTime; use InvalidArgumentException; -use Phinx\Config\Config; +use Migrations\Migration\BuiltinBackend; +use Migrations\Migration\PhinxBackend; use Phinx\Config\ConfigInterface; -use Phinx\Db\Adapter\WrapperInterface; -use Phinx\Migration\Manager; use RuntimeException; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; @@ -29,8 +28,6 @@ /** * The Migrations class is responsible for handling migrations command * within an none-shell application. - * - * TODO(mark) This needs to be adapted to use the configure backend selection. */ class Migrations { @@ -129,6 +126,24 @@ public function getCommand(): string return $this->command; } + /** + * Get the Migrations interface backend based on configuration data. + * + * @return \Migrations\Migration\BuiltinBackend|\Migrations\Migration\PhinxBackend + */ + protected function getBackend(): BuiltinBackend|PhinxBackend + { + $backend = (string)(Configure::read('Migrations.backend') ?? 'phinx'); + if ($backend === 'builtin') { + return new BuiltinBackend($this->default); + } + if ($backend === 'phinx') { + return new PhinxBackend($this->default); + } + + throw new RuntimeException("Unknown `Migrations.backend` of `{$backend}`"); + } + /** * Returns the status of each migrations based on the options passed * @@ -143,11 +158,9 @@ public function getCommand(): string */ public function status(array $options = []): array { - $this->setCommand('status'); - $input = $this->getInput('Status', [], $options); - $params = ['default', $input->getOption('format')]; + $backend = $this->getBackend(); - return $this->run('printStatus', $params, $input); + return $backend->status($options); } /** @@ -166,19 +179,9 @@ public function status(array $options = []): array */ public function migrate(array $options = []): bool { - $this->setCommand('migrate'); - $input = $this->getInput('Migrate', [], $options); - $method = 'migrate'; - $params = ['default', $input->getOption('target')]; - - if ($input->getOption('date')) { - $method = 'migrateToDateTime'; - $params[1] = new DateTime($input->getOption('date')); - } - - $this->run($method, $params, $input); + $backend = $this->getBackend(); - return true; + return $backend->migrate($options); } /** @@ -197,19 +200,9 @@ public function migrate(array $options = []): bool */ public function rollback(array $options = []): bool { - $this->setCommand('rollback'); - $input = $this->getInput('Rollback', [], $options); - $method = 'rollback'; - $params = ['default', $input->getOption('target')]; - - if ($input->getOption('date')) { - $method = 'rollbackToDateTime'; - $params[1] = new DateTime($input->getOption('date')); - } + $backend = $this->getBackend(); - $this->run($method, $params, $input); - - return true; + return $backend->rollback($options); } /** @@ -226,32 +219,9 @@ public function rollback(array $options = []): bool */ public function markMigrated(int|string|null $version = null, array $options = []): bool { - $this->setCommand('mark_migrated'); - - if ( - isset($options['target']) && - isset($options['exclude']) && - isset($options['only']) - ) { - $exceptionMessage = 'You should use `exclude` OR `only` (not both) along with a `target` argument'; - throw new InvalidArgumentException($exceptionMessage); - } + $backend = $this->getBackend(); - $input = $this->getInput('MarkMigrated', ['version' => $version], $options); - $this->setInput($input); - - // This will need to vary based on the config option. - $migrationPaths = $this->getConfig()->getMigrationPaths(); - $config = $this->getConfig(true); - $params = [ - array_pop($migrationPaths), - $this->getManager($config)->getVersionsToMark($input), - $this->output, - ]; - - $this->run('markVersionsAsMigrated', $params, $input); - - return true; + return $backend->markMigrated($version, $options); } /** @@ -268,76 +238,9 @@ public function markMigrated(int|string|null $version = null, array $options = [ */ public function seed(array $options = []): bool { - $this->setCommand('seed'); - $input = $this->getInput('Seed', [], $options); - - $seed = $input->getOption('seed'); - if (!$seed) { - $seed = null; - } - - $params = ['default', $seed]; - $this->run('seed', $params, $input); - - return true; - } - - /** - * Runs the method needed to execute and return - * - * @param string $method Manager method to call - * @param array $params Manager params to pass - * @param \Symfony\Component\Console\Input\InputInterface $input InputInterface needed for the - * Manager to properly run - * @return mixed The result of the CakeManager::$method() call - */ - protected function run(string $method, array $params, InputInterface $input): mixed - { - // This will need to vary based on the backend configuration - if ($this->configuration instanceof Config) { - $migrationPaths = $this->getConfig()->getMigrationPaths(); - $migrationPath = array_pop($migrationPaths); - $seedPaths = $this->getConfig()->getSeedPaths(); - $seedPath = array_pop($seedPaths); - } - - $pdo = null; - if ($this->manager instanceof Manager) { - $pdo = $this->manager->getEnvironment('default') - ->getAdapter() - ->getConnection(); - } - - $this->setInput($input); - $newConfig = $this->getConfig(true); - $manager = $this->getManager($newConfig); - $manager->setInput($input); - - // Why is this being done? Is this something we can eliminate in the new code path? - if ($pdo !== null) { - /** @var \Phinx\Db\Adapter\PdoAdapter|\Migrations\CakeAdapter $adapter */ - /** @psalm-suppress PossiblyNullReference */ - $adapter = $this->manager->getEnvironment('default')->getAdapter(); - while ($adapter instanceof WrapperInterface) { - /** @var \Phinx\Db\Adapter\PdoAdapter|\Migrations\CakeAdapter $adapter */ - $adapter = $adapter->getAdapter(); - } - $adapter->setConnection($pdo); - } - - $newMigrationPaths = $newConfig->getMigrationPaths(); - if (isset($migrationPath) && array_pop($newMigrationPaths) !== $migrationPath) { - $manager->resetMigrations(); - } - $newSeedPaths = $newConfig->getSeedPaths(); - if (isset($seedPath) && array_pop($newSeedPaths) !== $seedPath) { - $manager->resetSeeds(); - } - - /** @var callable $callable */ - $callable = [$manager, $method]; + $backend = $this->getBackend(); - return call_user_func_array($callable, $params); + return $backend->seed($options); } /** diff --git a/tests/TestCase/Command/Phinx/StatusTest.php b/tests/TestCase/Command/Phinx/StatusTest.php index 22e8fd3e..97f493d9 100644 --- a/tests/TestCase/Command/Phinx/StatusTest.php +++ b/tests/TestCase/Command/Phinx/StatusTest.php @@ -185,6 +185,7 @@ public function testExecuteWithInconsistency() $migrations = $this->getMigrations(); $migrations->migrate(); + $migrations = $this->getMigrations(); $migrationPaths = $migrations->getConfig()->getMigrationPaths(); $migrationPath = array_pop($migrationPaths); $origin = $migrationPath . DS . '20150724233100_update_numbers_table.php'; @@ -248,7 +249,14 @@ protected function getMigrations() 'connection' => 'test', 'source' => 'TestsMigrations', ]; + $args = [ + '--connection' => $params['connection'], + '--source' => $params['source'], + ]; + $input = new ArrayInput($args, $this->command->getDefinition()); $migrations = new Migrations($params); + $migrations->setInput($input); + $this->command->setInput($input); $adapter = $migrations ->getManager($this->command->getConfig()) diff --git a/tests/TestCase/Config/AbstractConfigTestCase.php b/tests/TestCase/Config/AbstractConfigTestCase.php index 5929161a..e0a0c0cf 100644 --- a/tests/TestCase/Config/AbstractConfigTestCase.php +++ b/tests/TestCase/Config/AbstractConfigTestCase.php @@ -55,7 +55,6 @@ public function getConfigArray() 'file' => '%%PHINX_CONFIG_PATH%%/tpl/testtemplate.txt', 'class' => '%%PHINX_CONFIG_PATH%%/tpl/testtemplate.php', ], - // TODO ideally we only need the connection and migration table name. 'environment' => $adapter, ]; } diff --git a/tests/TestCase/Migration/ManagerTest.php b/tests/TestCase/Migration/ManagerTest.php index 9e109235..aca9fc0c 100644 --- a/tests/TestCase/Migration/ManagerTest.php +++ b/tests/TestCase/Migration/ManagerTest.php @@ -134,7 +134,6 @@ protected function prepareEnvironment(array $paths = []): AdapterInterface $adapter = $connectionConfig['scheme'] ?? null; $adapterConfig = [ 'connection' => 'test', - // TODO all of this should go away 'adapter' => $adapter, 'user' => $connectionConfig['username'], 'pass' => $connectionConfig['password'], diff --git a/tests/TestCase/MigrationsTest.php b/tests/TestCase/MigrationsTest.php index b25c2c1f..3fc57022 100644 --- a/tests/TestCase/MigrationsTest.php +++ b/tests/TestCase/MigrationsTest.php @@ -20,7 +20,6 @@ use Cake\TestSuite\TestCase; use Exception; use InvalidArgumentException; -use Migrations\CakeAdapter; use Migrations\Migrations; use Phinx\Config\FeatureFlags; use Phinx\Db\Adapter\WrapperInterface; @@ -123,13 +122,24 @@ public function tearDown(): void FeatureFlags::setFlagsFromConfig(Configure::read('Migrations')); } + public static function backendProvider(): array + { + return [ + ['builtin'], + ['phinx'], + ]; + } + /** * Tests the status method * + * @dataProvider backendProvider * @return void */ - public function testStatus() + public function testStatus(string $backend) { + Configure::write('Migrations.backend', $backend); + $result = $this->migrations->status(); $expected = [ [ @@ -154,22 +164,18 @@ public function testStatus() ], ]; $this->assertEquals($expected, $result); - - $adapter = $this->migrations - ->getManager() - ->getEnvironment('default') - ->getAdapter(); - - $this->assertInstanceOf(CakeAdapter::class, $adapter); } /** * Tests the migrations and rollbacks * + * @dataProvider backendProvider * @return void */ - public function testMigrateAndRollback() + public function testMigrateAndRollback($backend) { + Configure::write('Migrations.backend', $backend); + if ($this->Connection->getDriver() instanceof Sqlserver) { // TODO This test currently fails in CI because numbers table // has no columns in sqlserver. This table should have columns as the @@ -252,10 +258,13 @@ public function testMigrateAndRollback() /** * Tests the collation table behavior when using MySQL * + * @dataProvider backendProvider * @return void */ - public function testCreateWithEncoding() + public function testCreateWithEncoding($backend) { + Configure::write('Migrations.backend', $backend); + $this->skipIf(env('DB') !== 'mysql', 'Requires MySQL'); $migrate = $this->migrations->migrate(); @@ -278,10 +287,13 @@ public function testCreateWithEncoding() * Tests calling Migrations::markMigrated without params marks everything * as migrated * + * @dataProvider backendProvider * @return void */ - public function testMarkMigratedAll() + public function testMarkMigratedAll($backend) { + Configure::write('Migrations.backend', $backend); + $markMigrated = $this->migrations->markMigrated(); $this->assertTrue($markMigrated); $status = $this->migrations->status(); @@ -315,10 +327,13 @@ public function testMarkMigratedAll() * string 'all' marks everything * as migrated * + * @dataProvider backendProvider * @return void */ - public function testMarkMigratedAllAsVersion() + public function testMarkMigratedAllAsVersion($backend) { + Configure::write('Migrations.backend', $backend); + $markMigrated = $this->migrations->markMigrated('all'); $this->assertTrue($markMigrated); $status = $this->migrations->status(); @@ -351,10 +366,13 @@ public function testMarkMigratedAllAsVersion() * Tests calling Migrations::markMigrated with the target option will mark * only up to that one * + * @dataProvider backendProvider * @return void */ - public function testMarkMigratedTarget() + public function testMarkMigratedTarget($backend) { + Configure::write('Migrations.backend', $backend); + $markMigrated = $this->migrations->markMigrated(null, ['target' => '20150704160200']); $this->assertTrue($markMigrated); $status = $this->migrations->status(); @@ -393,10 +411,13 @@ public function testMarkMigratedTarget() * Tests calling Migrations::markMigrated with the target option set to a * non-existent target will throw an exception * + * @dataProvider backendProvider * @return void */ - public function testMarkMigratedTargetError() + public function testMarkMigratedTargetError($backend) { + Configure::write('Migrations.backend', $backend); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Migration `20150704160610` was not found !'); $this->migrations->markMigrated(null, ['target' => '20150704160610']); @@ -406,10 +427,13 @@ public function testMarkMigratedTargetError() * Tests calling Migrations::markMigrated with the target option with the exclude * option will mark only up to that one, excluding it * + * @dataProvider backendProvider * @return void */ - public function testMarkMigratedTargetExclude() + public function testMarkMigratedTargetExclude($backend) { + Configure::write('Migrations.backend', $backend); + $markMigrated = $this->migrations->markMigrated(null, ['target' => '20150704160200', 'exclude' => true]); $this->assertTrue($markMigrated); $status = $this->migrations->status(); @@ -448,10 +472,13 @@ public function testMarkMigratedTargetExclude() * Tests calling Migrations::markMigrated with the target option with the only * option will mark only that specific migrations * + * @dataProvider backendProvider * @return void */ - public function testMarkMigratedTargetOnly() + public function testMarkMigratedTargetOnly($backend) { + Configure::write('Migrations.backend', $backend); + $markMigrated = $this->migrations->markMigrated(null, ['target' => '20150724233100', 'only' => true]); $this->assertTrue($markMigrated); $status = $this->migrations->status(); @@ -490,10 +517,13 @@ public function testMarkMigratedTargetOnly() * Tests calling Migrations::markMigrated with the target option, the only option * and the exclude option will throw an exception * + * @dataProvider backendProvider * @return void */ - public function testMarkMigratedTargetExcludeOnly() + public function testMarkMigratedTargetExcludeOnly($backend) { + Configure::write('Migrations.backend', $backend); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('You should use `exclude` OR `only` (not both) along with a `target` argument'); $this->migrations->markMigrated(null, ['target' => '20150724233100', 'only' => true, 'exclude' => true]); @@ -503,10 +533,13 @@ public function testMarkMigratedTargetExcludeOnly() * Tests calling Migrations::markMigrated with the target option with the exclude * option will mark only up to that one, excluding it * + * @dataProvider backendProvider * @return void */ - public function testMarkMigratedVersion() + public function testMarkMigratedVersion($backend) { + Configure::write('Migrations.backend', $backend); + $markMigrated = $this->migrations->markMigrated(20150704160200); $this->assertTrue($markMigrated); $status = $this->migrations->status(); @@ -545,10 +578,13 @@ public function testMarkMigratedVersion() * Tests that calling the migrations methods while passing * parameters will override the default ones * + * @dataProvider backendProvider * @return void */ - public function testOverrideOptions() + public function testOverrideOptions($backend) { + Configure::write('Migrations.backend', $backend); + $result = $this->migrations->status(); $expectedStatus = [ [ @@ -613,10 +649,13 @@ public function testOverrideOptions() * Tests that calling the migrations methods while passing the ``date`` * parameter works as expected * + * @dataProvider backendProvider * @return void */ - public function testMigrateDateOption() + public function testMigrateDateOption($backend) { + Configure::write('Migrations.backend', $backend); + // If we want to migrate to a date before the first first migration date, // we should not migrate anything $this->migrations->migrate(['date' => '20140705']); @@ -789,10 +828,13 @@ public function testMigrateDateOption() /** * Tests seeding the database * + * @dataProvider backendProvider * @return void */ - public function testSeed() + public function testSeed($backend) { + Configure::write('Migrations.backend', $backend); + $this->migrations->migrate(); $seed = $this->migrations->seed(['source' => 'Seeds']); $this->assertTrue($seed); @@ -865,10 +907,13 @@ public function testSeed() /** * Tests seeding the database with seeder * + * @dataProvider backendProvider * @return void */ - public function testSeedOneSeeder() + public function testSeedOneSeeder($backend) { + Configure::write('Migrations.backend', $backend); + $this->migrations->migrate(); $seed = $this->migrations->seed(['source' => 'AltSeeds', 'seed' => 'AnotherNumbersSeed']); @@ -914,10 +959,13 @@ public function testSeedOneSeeder() /** * Tests seeding the database with seeder * + * @dataProvider backendProvider * @return void */ - public function testSeedCallSeeder() + public function testSeedCallSeeder($backend) { + Configure::write('Migrations.backend', $backend); + $this->migrations->migrate(); $seed = $this->migrations->seed(['source' => 'CallSeeds', 'seed' => 'DatabaseSeed']); @@ -975,15 +1023,33 @@ public function testSeedCallSeeder() /** * Tests that requesting a unexistant seed throws an exception * + * @dataProvider backendProvider * @return void */ - public function testSeedWrongSeed() + public function testSeedWrongSeed($backend) { + Configure::write('Migrations.backend', $backend); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The seed class "DerpSeed" does not exist'); $this->migrations->seed(['source' => 'AltSeeds', 'seed' => 'DerpSeed']); } + /** + * Tests migrating the baked snapshots with builtin backend + * + * @dataProvider snapshotMigrationsProvider + * @param string $basePath Snapshot file path + * @param string $filename Snapshot file name + * @param array $flags Feature flags + * @return void + */ + public function testMigrateSnapshotsBuiltin(string $basePath, string $filename, array $flags = []): void + { + Configure::write('Migrations.backend', 'builtin'); + $this->runMigrateSnapshots($basePath, $filename, $flags); + } + /** * Tests migrating the baked snapshots * @@ -993,7 +1059,12 @@ public function testSeedWrongSeed() * @param array $flags Feature flags * @return void */ - public function testMigrateSnapshots(string $basePath, string $filename, array $flags = []): void + public function testMigrateSnapshotsPhinx(string $basePath, string $filename, array $flags = []): void + { + $this->runMigrateSnapshots($basePath, $filename, $flags); + } + + protected function runMigrateSnapshots(string $basePath, string $filename, array $flags): void { if ($this->Connection->getDriver() instanceof Sqlserver) { // TODO once migrations is using the inlined sqlserver adapter, this skip should @@ -1040,9 +1111,13 @@ public function testMigrateSnapshots(string $basePath, string $filename, array $ /** * Tests that migrating in case of error throws an exception + * + * @dataProvider backendProvider */ - public function testMigrateErrors() + public function testMigrateErrors($backend) { + Configure::write('Migrations.backend', $backend); + $this->expectException(Exception::class); $this->migrations->markMigrated(20150704160200); $this->migrations->migrate(); @@ -1050,9 +1125,13 @@ public function testMigrateErrors() /** * Tests that rolling back in case of error throws an exception + * + * @dataProvider backendProvider */ - public function testRollbackErrors() + public function testRollbackErrors($backend) { + Configure::write('Migrations.backend', $backend); + $this->expectException(Exception::class); $this->migrations->markMigrated('all'); $this->migrations->rollback(); @@ -1061,9 +1140,13 @@ public function testRollbackErrors() /** * Tests that marking migrated a non-existant migrations returns an error * and can return a error message + * + * @dataProvider backendProvider */ - public function testMarkMigratedErrors() + public function testMarkMigratedErrors($backend) { + Configure::write('Migrations.backend', $backend); + $this->expectException(Exception::class); $this->migrations->markMigrated(20150704000000); }