<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations;
use Phinx\Migration\Manager;
use Symfony\Component\Console\Input\InputInterface;
/**
* Overrides Phinx Manager class in order to provide an interface
* for running migrations within an app
*/
class CakeManager extends Manager
{
public $maxNameLength = 0;
/**
* Instance of InputInterface the Manager is dealing with for the current shell call
*
* @var \Symfony\Component\Console\Input\InputInterface
*/
protected $input;
/**
* Reset the migrations stored in the object
*
* @return void
*/
public function resetMigrations()
{
$this->migrations = null;
}
/**
* Reset the seeds stored in the object
*
* @return void
*/
public function resetSeeds()
{
$this->seeds = null;
}
/**
* Prints the specified environment's migration status.
*
* @param string $environment Environment name.
* @param null|string $format Format (`json` or `array`).
* @return array|string Array of migrations or json string.
*/
public function printStatus($environment, $format = null)
{
$migrations = [];
$isJson = $format === 'json';
if (count($this->getMigrations('default'))) {
$env = $this->getEnvironment($environment);
$versions = $env->getVersionLog();
$this->maxNameLength = $versions ? max(array_map(function ($version) {
return strlen($version['migration_name']);
}, $versions)) : 0;
foreach ($this->getMigrations('default') as $migration) {
if (array_key_exists($migration->getVersion(), $versions)) {
$status = 'up';
unset($versions[$migration->getVersion()]);
} else {
$status = 'down';
}
$version = $migration->getVersion();
$migrationParams = [
'status' => $status,
'id' => $migration->getVersion(),
'name' => $migration->getName(),
];
$migrations[$version] = $migrationParams;
}
foreach ($versions as $missing) {
$version = $missing['version'];
$migrationParams = [
'status' => 'up',
'id' => $version,
'name' => $missing['migration_name'],
];
if (!$isJson) {
$migrationParams = array_merge($migrationParams, [
'missing' => true,
]);
}
$migrations[$version] = $migrationParams;
}
}
ksort($migrations);
$migrations = array_values($migrations);
if ($isJson) {
$migrations = json_encode($migrations);
}
return $migrations;
}
/**
* {@inheritdoc}
*/
public function migrateToDateTime($environment, \DateTime $dateTime, $fake = false)
{
$versions = array_keys($this->getMigrations('default'));
$dateString = $dateTime->format('Ymdhis');
$versionToMigrate = null;
foreach ($versions as $version) {
if ($dateString > $version) {
$versionToMigrate = $version;
}
}
if ($versionToMigrate === null) {
$this->getOutput()->writeln(
'No migrations to run'
);
return;
}
$this->getOutput()->writeln(
'Migrating to version ' . $versionToMigrate
);
$this->migrate($environment, $versionToMigrate, $fake);
}
/**
* {@inheritdoc}
*/
public function rollbackToDateTime($environment, \DateTime $dateTime, $force = false)
{
$env = $this->getEnvironment($environment);
$versions = $env->getVersions();
$dateString = $dateTime->format('Ymdhis');
sort($versions);
$versions = array_reverse($versions);
if (empty($versions) || $dateString > $versions[0]) {
$this->getOutput()->writeln('No migrations to rollback');
return;
}
if ($dateString < end($versions)) {
$this->getOutput()->writeln('Rolling back all migrations');
$this->rollback($environment, 0);
return;
}
$index = 0;
foreach ($versions as $index => $version) {
if ($dateString > $version) {
break;
}
}
$versionToRollback = $versions[$index];
$this->getOutput()->writeln('Rolling back to version ' . $versionToRollback);
$this->rollback($environment, $versionToRollback, $force);
}
/**
* Checks if the migration with version number $version as already been mark migrated
*
* @param int|string $version Version number of the migration to check
* @return bool
*/
public function isMigrated($version)
{
$adapter = $this->getEnvironment('default')->getAdapter();
$versions = array_flip($adapter->getVersions());
return isset($versions[$version]);
}
/**
* Marks migration with version number $version migrated
*
* @param int|string $version Version number of the migration to check
* @param string $path Path where the migration file is located
* @return bool True if success
*/
public function markMigrated($version, $path)
{
$adapter = $this->getEnvironment('default')->getAdapter();
$migrationFile = glob($path . DS . $version . '*');
if (empty($migrationFile)) {
throw new \RuntimeException(
sprintf('A migration file matching version number `%s` could not be found', $version)
);
}
$migrationFile = $migrationFile[0];
$className = $this->getMigrationClassName($migrationFile);
require_once $migrationFile;
$Migration = new $className('default', $version);
$time = date('Y-m-d H:i:s', time());
$adapter->migrated($Migration, 'up', $time, $time);
return true;
}
/**
* Decides which versions it should mark as migrated
*
* @param \Symfony\Component\Console\Input\InputInterface $input Input interface from which argument and options
* will be extracted to determine which versions to be marked as migrated
* @return array Array of versions that should be marked as migrated
* @throws \InvalidArgumentException If the `--exclude` or `--only` options are used without `--target`
* or version not found
*/
public function getVersionsToMark($input)
{
$migrations = $this->getMigrations('default');
$versions = array_keys($migrations);
$versionArg = $input->getArgument('version');
$targetArg = $input->getOption('target');
$hasAllVersion = in_array($versionArg, ['all', '*']);
if ((empty($versionArg) && empty($targetArg)) || $hasAllVersion) {
return $versions;
}
$version = $targetArg ?: $versionArg;
if ($input->getOption('only') || !empty($versionArg)) {
if (!in_array($version, $versions)) {
throw new \InvalidArgumentException("Migration `$version` was not found !");
}
return [$version];
}
$lengthIncrease = $input->getOption('exclude') ? 0 : 1;
$index = array_search($version, $versions);
if ($index === false) {
throw new \InvalidArgumentException("Migration `$version` was not found !");
}
return array_slice($versions, 0, $index + $lengthIncrease);
}
/**
* Mark all migrations in $versions array found in $path as migrated
*
* It will start a transaction and rollback in case one of the operation raises an exception
*
* @param string $path Path where to look for migrations
* @param array $versions Versions which should be marked
* @param \Symfony\Component\Console\Output\OutputInterface $output OutputInterface used to store
* the command output
* @return void
*/
public function markVersionsAsMigrated($path, $versions, $output)
{
$adapter = $this->getEnvironment('default')->getAdapter();
if (empty($versions)) {
$output->writeln('<info>No migrations were found. Nothing to mark as migrated.</info>');
return;
}
$adapter->beginTransaction();
foreach ($versions as $version) {
if ($this->isMigrated($version)) {
$output->writeln(sprintf('<info>Skipping migration `%s` (already migrated).</info>', $version));
continue;
}
try {
$this->markMigrated($version, $path);
$output->writeln(
sprintf('<info>Migration `%s` successfully marked migrated !</info>', $version)
);
} catch (\Exception $e) {
$adapter->rollbackTransaction();
$output->writeln(
sprintf(
'<error>An error occurred while marking migration `%s` as migrated : %s</error>',
$version,
$e->getMessage()
)
);
$output->writeln('<error>All marked migrations during this process were unmarked.</error>');
return;
}
}
$adapter->commitTransaction();
}
/**
* Resolves a migration class name based on $path
*
* @param string $path Path to the migration file of which we want the class name
* @return string Migration class name
*/
protected function getMigrationClassName($path)
{
$class = preg_replace('/^[0-9]+_/', '', basename($path));
$class = str_replace('_', ' ', $class);
$class = ucwords($class);
$class = str_replace(' ', '', $class);
if (strpos($class, '.') !== false) {
$class = substr($class, 0, strpos($class, '.'));
}
return $class;
}
/**
* Sets the InputInterface the Manager is dealing with for the current shell call
*
* @param \Symfony\Component\Console\Input\InputInterface $input Instance of InputInterface
* @return void
*/
public function setInput(InputInterface $input)
{
$this->input = $input;
}
/**
* Overload the basic behavior to add an instance of the InputInterface the shell call is
* using in order to gives the ability to the AbstractSeed::call() method to propagate options
* to the other MigrationsDispatcher it is generating.
*
* {@inheritdoc}
*/
public function getSeeds()
{
parent::getSeeds();
if (empty($this->seeds)) {
return $this->seeds;
}
foreach ($this->seeds as $class => $instance) {
if ($instance instanceof AbstractSeed) {
$instance->setInput($this->input);
}
}
return $this->seeds;
}
}