Actualización

This commit is contained in:
Xes
2025-04-10 12:24:57 +02:00
parent 8969cc929d
commit 45420b6f0d
39760 changed files with 4303286 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
<?php
namespace Doctrine\DBAL\Migrations;
class AbortMigrationException extends MigrationException
{
}

View File

@@ -0,0 +1,178 @@
<?php
namespace Doctrine\DBAL\Migrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration as NewAbstractMigration;
/**
* Abstract class for individual migrations to extend from.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan H. Wage <jonwage@gmail.com>
* @deprecated Please use Doctrine\Migrations\AbstractMigration
*/
abstract class AbstractMigration
{
/**
* Reference to the Version instance representing this migration
*
* @var Version
*/
protected $version;
/**
* The Doctrine\DBAL\Connection instance we are migrating
*
* @var \Doctrine\DBAL\Connection
*/
protected $connection;
/**
* Reference to the SchemaManager instance referenced by $_connection
*
* @var \Doctrine\DBAL\Schema\AbstractSchemaManager
*/
protected $sm;
/**
* Reference to the DatabasePlatform instance referenced by $_connection
*
* @var \Doctrine\DBAL\Platforms\AbstractPlatform
*/
protected $platform;
/**
* The OutputWriter object instance used for outputting information
*
* @var OutputWriter
*/
private $outputWriter;
public function __construct(Version $version)
{
if ( ! $this instanceof NewAbstractMigration) {
@trigger_error(sprintf('The "%s" class is deprecated since Doctrine Migrations 2.0. Use %s instead.', AbstractMigration::class, NewAbstractMigration::class), E_USER_DEPRECATED);
}
$config = $version->getConfiguration();
$this->version = $version;
$this->connection = $config->getConnection();
$this->sm = $this->connection->getSchemaManager();
$this->platform = $this->connection->getDatabasePlatform();
$this->outputWriter = $config->getOutputWriter();
}
/**
* Indicates the transactional mode of this migration.
* If this function returns true (default) the migration will be executed in one transaction,
* otherwise non-transactional state will be used to execute each of the migration SQLs.
*
* Extending class should override this function to alter the return value
*
* @return bool TRUE by default.
*/
public function isTransactional()
{
return true;
}
/**
* Get migration description
*
* @return string
*/
public function getDescription()
{
return '';
}
/**
* Print a warning message if the condition evaluates to TRUE.
*
* @param boolean $condition
* @param string $message
*/
public function warnIf($condition, $message = '')
{
if ($condition) {
$message = $message ?: 'Unknown Reason';
$this->outputWriter->write(sprintf(
' <comment>Warning during %s: %s</comment>',
$this->version->getExecutionState(),
$message
));
}
}
/**
* Abort the migration if the condition evaluates to TRUE.
*
* @param boolean $condition
* @param string $message
*
* @throws AbortMigrationException
*/
public function abortIf($condition, $message = '')
{
if ($condition) {
throw new AbortMigrationException($message ?: 'Unknown Reason');
}
}
/**
* Skip this migration (but not the next ones) if condition evaluates to TRUE.
*
* @param boolean $condition
* @param string $message
*
* @throws SkipMigrationException
*/
public function skipIf($condition, $message = '')
{
if ($condition) {
throw new SkipMigrationException($message ?: 'Unknown Reason');
}
}
public function preUp(Schema $schema)
{
}
public function postUp(Schema $schema)
{
}
public function preDown(Schema $schema)
{
}
public function postDown(Schema $schema)
{
}
abstract public function up(Schema $schema);
abstract public function down(Schema $schema);
protected function addSql($sql, array $params = [], array $types = [])
{
$this->version->addSql($sql, $params, $types);
}
protected function write($message)
{
$this->outputWriter->write($message);
}
protected function throwIrreversibleMigrationException($message = null)
{
if (null === $message) {
$message = 'This migration is irreversible and cannot be reverted.';
}
throw new IrreversibleMigrationException($message);
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace Doctrine\DBAL\Migrations\Configuration;
use Doctrine\DBAL\Migrations\MigrationException;
/**
* Abstract Migration Configuration class for loading configuration information
* from a configuration file (xml or yml).
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan H. Wage <jonwage@gmail.com>
*/
abstract class AbstractFileConfiguration extends Configuration
{
/**
* The configuration file used to load configuration information
*
* @var string
*/
private $file;
/**
* Whether or not the configuration file has been loaded yet or not
*
* @var boolean
*/
private $loaded = false;
/**
* @var array of possible configuration properties in migrations configuration.
*/
private $configurationProperties = [
'migrations_namespace' => 'setMigrationsNamespace',
'table_name' => 'setMigrationsTableName',
'column_name' => 'setMigrationsColumnName',
'organize_migrations' => 'setMigrationOrganisation',
'name' => 'setName',
'migrations_directory' => 'loadMigrationsFromDirectory',
'migrations' => 'loadMigrations',
'custom_template' => 'setCustomTemplate',
];
protected function setConfiguration(array $config)
{
foreach ($config as $configurationKey => $configurationValue) {
if ( ! isset($this->configurationProperties[$configurationKey])) {
$msg = sprintf('Migrations configuration key "%s" does not exist.', $configurationKey);
throw MigrationException::configurationNotValid($msg);
}
}
foreach ($this->configurationProperties as $configurationKey => $configurationSetter) {
if (isset($config[$configurationKey])) {
$this->{$configurationSetter}($config[$configurationKey]);
}
}
}
private function loadMigrationsFromDirectory($migrationsDirectory)
{
$this->setMigrationsDirectory($migrationsDirectory);
$this->registerMigrationsFromDirectory($migrationsDirectory);
}
private function loadMigrations($migrations)
{
if (is_array($migrations)) {
foreach ($migrations as $migration) {
$this->registerMigration($migration['version'], $migration['class']);
}
}
}
private function setMigrationOrganisation($migrationOrganisation)
{
if (strcasecmp($migrationOrganisation, static::VERSIONS_ORGANIZATION_BY_YEAR) == 0) {
$this->setMigrationsAreOrganizedByYear();
} elseif (strcasecmp($migrationOrganisation, static::VERSIONS_ORGANIZATION_BY_YEAR_AND_MONTH) == 0) {
$this->setMigrationsAreOrganizedByYearAndMonth();
} else {
$msg = 'Unknown ' . var_export($migrationOrganisation, true) . ' for configuration "organize_migrations".';
throw MigrationException::configurationNotValid($msg);
}
}
/**
* Load the information from the passed configuration file
*
* @param string $file The path to the configuration file
*
* @throws MigrationException Throws exception if configuration file was already loaded
*/
public function load($file)
{
if ($this->loaded) {
throw MigrationException::configurationFileAlreadyLoaded();
}
if (file_exists($path = getcwd() . '/' . $file)) {
$file = $path;
}
$this->file = $file;
if ( ! file_exists($file)) {
throw new \InvalidArgumentException('Given config file does not exist');
}
$this->doLoad($file);
$this->loaded = true;
}
protected function getDirectoryRelativeToFile($file, $input)
{
$path = realpath(dirname($file) . '/' . $input);
return ($path !== false) ? $path : $input;
}
public function getFile()
{
return $this->file;
}
/**
* Abstract method that each file configuration driver must implement to
* load the given configuration file whether it be xml, yaml, etc. or something
* else.
*
* @param string $file The path to a configuration file.
*/
abstract protected function doLoad($file);
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Doctrine\DBAL\Migrations\Configuration;
/**
* Load migration configuration information from a PHP configuration file.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author David Havl <contact@davidhavl.com>
*/
class ArrayConfiguration extends AbstractFileConfiguration
{
/**
* @inheritdoc
*/
protected function doLoad($file)
{
$config = require $file;
if (isset($config['migrations_directory'])) {
$config['migrations_directory'] = $this->getDirectoryRelativeToFile($file, $config['migrations_directory']);
}
$this->setConfiguration($config);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
<?php
namespace Doctrine\DBAL\Migrations\Configuration\Connection;
use Doctrine\DBAL\Connection;
interface ConnectionLoaderInterface
{
/**
* read the input and return a Configuration, returns `false` if the config
* is not supported
* @return Connection|null
*/
public function chosen();
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Doctrine\DBAL\Migrations\Configuration\Connection\Loader;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Migrations\Configuration\Connection\ConnectionLoaderInterface;
class ArrayConnectionConfigurationLoader implements ConnectionLoaderInterface
{
private $filename;
public function __construct($filename)
{
$this->filename = $filename;
}
/**
* read the input and return a Configuration, returns `false` if the config
* is not supported
* @return Connection|null
*/
public function chosen()
{
if (empty($this->filename)) {
return null;
}
if ( ! file_exists($this->filename)) {
return null;
}
$params = include $this->filename;
if ( ! is_array($params)) {
throw new \InvalidArgumentException('The connection file has to return an array with database configuration parameters.');
}
return DriverManager::getConnection($params);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Doctrine\DBAL\Migrations\Configuration\Connection\Loader;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Migrations\Configuration\Connection\ConnectionLoaderInterface;
final class ConnectionConfigurationChainLoader implements ConnectionLoaderInterface
{
/** @var ConnectionLoaderInterface[] */
private $loaders;
public function __construct(array $loaders)
{
$this->loaders = $loaders;
}
/**
* read the input and return a Configuration, returns `false` if the config
* is not supported
* @return Connection|null
*/
public function chosen()
{
foreach ($this->loaders as $loader) {
if (null !== $confObj = $loader->chosen()) {
return $confObj;
}
}
return null;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Doctrine\DBAL\Migrations\Configuration\Connection\Loader;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Doctrine\DBAL\Migrations\Configuration\Connection\ConnectionLoaderInterface;
class ConnectionConfigurationLoader implements ConnectionLoaderInterface
{
/** @var Configuration */
private $configuration;
public function __construct(Configuration $configuration = null)
{
if ($configuration !== null) {
$this->configuration = $configuration;
}
}
/**
* read the input and return a Configuration, returns `false` if the config
* is not supported
* @return Connection|null
*/
public function chosen()
{
if ($this->configuration) {
return $this->configuration->getConnection();
}
return null;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Doctrine\DBAL\Migrations\Configuration\Connection\Loader;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Migrations\Configuration\Connection\ConnectionLoaderInterface;
use Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper;
use Symfony\Component\Console\Helper\HelperSet;
class ConnectionHelperLoader implements ConnectionLoaderInterface
{
/**
* @var string
*/
private $helperName;
/** @var HelperSet */
private $helperSet;
/**
* ConnectionHelperLoader constructor.
* @param HelperSet $helperSet
* @param string $helperName
*/
public function __construct(HelperSet $helperSet = null, $helperName)
{
$this->helperName = $helperName;
if ($helperSet === null) {
$helperSet = new HelperSet();
}
$this->helperSet = $helperSet;
}
/**
* read the input and return a Configuration, returns `false` if the config
* is not supported
* @return Connection|null
*/
public function chosen()
{
if ($this->helperSet->has($this->helperName)) {
$connectionHelper = $this->helperSet->get($this->helperName);
if ($connectionHelper instanceof ConnectionHelper) {
return $connectionHelper->getConnection();
}
}
return null;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Doctrine\DBAL\Migrations\Configuration;
/**
* Load migration configuration information from a PHP configuration file.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author David Havl <contact@davidhavl.com>
*/
class JsonConfiguration extends AbstractFileConfiguration
{
/**
* @inheritdoc
*/
protected function doLoad($file)
{
$config = json_decode(file_get_contents($file), true);
if (isset($config['migrations_directory'])) {
$config['migrations_directory'] = $this->getDirectoryRelativeToFile($file, $config['migrations_directory']);
}
$this->setConfiguration($config);
}
}

View File

@@ -0,0 +1,40 @@
<xs:schema
attributeFormDefault="unqualified"
elementFormDefault="qualified"
targetNamespace="http://doctrine-project.org/schemas/migrations/configuration"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="doctrine-migrations">
<xs:complexType>
<xs:all minOccurs="0">
<xs:element type="xs:string" name="name" minOccurs="0" maxOccurs="1"/>
<xs:element type="xs:string" name="migrations-namespace" minOccurs="0" maxOccurs="1"/>
<xs:element name="table" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute type="xs:string" name="name"/>
<xs:attribute type="xs:string" name="column"/>
</xs:complexType>
</xs:element>
<xs:element name="organize-migrations" minOccurs="0" maxOccurs="1">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="((y|Y)(e|E)(a|A)(r|R))|((y|Y)(e|E)(a|A)(r|R)_(a|A)(n|N)(d|D)_(m|M)(o|O)(n|N)(t|T)(h|H))"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element type="xs:string" name="migrations-directory" minOccurs="0" maxOccurs="1"/>
<xs:element name="migrations" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="migration">
<xs:complexType mixed="true">
<xs:attribute name="version" type="xs:string"/>
<xs:attribute name="class" type="xs:string"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:all>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@@ -0,0 +1,57 @@
<?php
namespace Doctrine\DBAL\Migrations\Configuration;
use Doctrine\DBAL\Migrations\MigrationException;
/**
* Load migration configuration information from a XML configuration file.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan H. Wage <jonwage@gmail.com>
*/
class XmlConfiguration extends AbstractFileConfiguration
{
/**
* @inheritdoc
*/
protected function doLoad($file)
{
libxml_use_internal_errors(true);
$xml = new \DOMDocument();
$xml->load($file);
if ( ! $xml->schemaValidate(__DIR__ . DIRECTORY_SEPARATOR . "XML" . DIRECTORY_SEPARATOR . "configuration.xsd")) {
libxml_clear_errors();
throw MigrationException::configurationNotValid('XML configuration did not pass the validation test.');
}
$xml = simplexml_load_file($file, "SimpleXMLElement", LIBXML_NOCDATA);
$config = [];
if (isset($xml->name)) {
$config['name'] = (string) $xml->name;
}
if (isset($xml->table['name'])) {
$config['table_name'] = (string) $xml->table['name'];
}
if (isset($xml->table['column'])) {
$config['column_name'] = (string) $xml->table['column'];
}
if (isset($xml->{'migrations-namespace'})) {
$config['migrations_namespace'] = (string) $xml->{'migrations-namespace'};
}
if (isset($xml->{'organize-migrations'})) {
$config['organize_migrations'] = $xml->{'organize-migrations'};
}
if (isset($xml->{'migrations-directory'})) {
$config['migrations_directory'] = $this->getDirectoryRelativeToFile($file, (string) $xml->{'migrations-directory'});
}
if (isset($xml->migrations->migration)) {
$config['migrations'] = $xml->migrations->migration;
}
$this->setConfiguration($config);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Doctrine\DBAL\Migrations\Configuration;
use Symfony\Component\Yaml\Yaml;
use Doctrine\DBAL\Migrations\MigrationException;
/**
* Load migration configuration information from a YAML configuration file.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan H. Wage <jonwage@gmail.com>
*/
class YamlConfiguration extends AbstractFileConfiguration
{
/**
* @inheritdoc
*/
protected function doLoad($file)
{
if ( ! class_exists(Yaml::class)) {
throw MigrationException::yamlConfigurationNotAvailable();
}
$config = Yaml::parse(file_get_contents($file));
if ( ! is_array($config)) {
throw new \InvalidArgumentException('Not valid configuration.');
}
if (isset($config['migrations_directory'])) {
$config['migrations_directory'] = $this->getDirectoryRelativeToFile($file, $config['migrations_directory']);
}
$this->setConfiguration($config);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Doctrine\DBAL\Migrations\Event\Listeners;
use Doctrine\Common\EventSubscriber;
use Doctrine\DBAL\Migrations\Events;
use Doctrine\DBAL\Migrations\Event\MigrationsEventArgs;
/**
* Listens for `onMigrationsMigrated` and, if the conneciton is has autocommit
* makes sure to do the final commit to ensure changes stick around.
*
* @since 1.6
*/
final class AutoCommitListener implements EventSubscriber
{
public function onMigrationsMigrated(MigrationsEventArgs $args)
{
$conn = $args->getConnection();
if ( ! $args->isDryRun() && ! $conn->isAutoCommit()) {
$conn->commit();
}
}
/**
* {@inheritdoc}
*/
public function getSubscribedEvents()
{
return [Events::onMigrationsMigrated];
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Doctrine\DBAL\Migrations\Event;
use Doctrine\Common\EventArgs;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
class MigrationsEventArgs extends EventArgs
{
/**
* @var Configuration
*/
private $config;
/**
* The direction of the migration.
*
* @var string (up|down)
*/
private $direction;
/**
* Whether or not the migrations are executing in dry run mode.
*
* @var bool
*/
private $dryRun;
public function __construct(Configuration $config, $direction, $dryRun)
{
$this->config = $config;
$this->direction = $direction;
$this->dryRun = (bool) $dryRun;
}
public function getConfiguration()
{
return $this->config;
}
public function getConnection()
{
return $this->config->getConnection();
}
public function getDirection()
{
return $this->direction;
}
public function isDryRun()
{
return $this->dryRun;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Doctrine\DBAL\Migrations\Event;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Doctrine\DBAL\Migrations\Version;
class MigrationsVersionEventArgs extends MigrationsEventArgs
{
/**
* The version the event pertains to.
*
* @var Version
*/
private $version;
public function __construct(Version $version, Configuration $config, $direction, $dryRun)
{
parent::__construct($config, $direction, $dryRun);
$this->version = $version;
}
public function getVersion()
{
return $this->version;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Doctrine\DBAL\Migrations;
final class Events
{
/**
* Private constructor. This class cannot be instantiated.
*/
private function __construct()
{
}
const onMigrationsMigrating = 'onMigrationsMigrating';
const onMigrationsMigrated = 'onMigrationsMigrated';
const onMigrationsVersionExecuting = 'onMigrationsVersionExecuting';
const onMigrationsVersionExecuted = 'onMigrationsVersionExecuted';
const onMigrationsVersionSkipped = 'onMigrationsVersionSkipped';
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Migrations;
/**
* @since 1.6.0
* @author Luís Cobucci <lcobucci@gmail.com>
*/
final class FileQueryWriter implements QueryWriter
{
/**
* @var string
*/
private $columnName;
/**
* @var string
*/
private $tableName;
/**
* @var null|OutputWriter
*/
private $outputWriter;
public function __construct(string $columnName, string $tableName, ?OutputWriter $outputWriter)
{
$this->columnName = $columnName;
$this->tableName = $tableName;
$this->outputWriter = $outputWriter;
}
/**
* TODO: move SqlFileWriter's behaviour to this class - and kill it with fire (on the next major release)
* @param string $path
* @param string $direction
* @param array $queriesByVersion
* @return bool
*/
public function write(string $path, string $direction, array $queriesByVersion) : bool
{
$writer = new SqlFileWriter(
$this->columnName,
$this->tableName,
$path,
$this->outputWriter
);
// SqlFileWriter#write() returns `bool|int` but all clients expect it to be `bool` only
return (bool) $writer->write($queriesByVersion, $direction);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Doctrine\DBAL\Migrations\Finder;
/**
* Abstract base class for MigrationFinders
*
* @since 1.0.0-alpha3
*/
abstract class AbstractFinder implements MigrationFinderInterface
{
protected static function requireOnce($path)
{
require_once $path;
}
protected function getRealPath($directory)
{
$dir = realpath($directory);
if (false === $dir || ! is_dir($dir)) {
throw new \InvalidArgumentException(sprintf(
'Cannot load migrations from "%s" because it is not a valid directory',
$directory
));
}
return $dir;
}
/**
* Load the migrations and return an array of thoses loaded migrations
* @param array $files array of migration filename found
* @param string $namespace namespace of thoses migrations
* @return array constructed with the migration name as key and the value is the fully qualified name of the migration
*/
protected function loadMigrations($files, $namespace)
{
$migrations = [];
uasort($files, $this->getFileSortCallback());
foreach ($files as $file) {
static::requireOnce($file);
$className = basename($file, '.php');
$version = (string) substr($className, 7);
if ($version === '0') {
throw new \InvalidArgumentException(sprintf(
'Cannot load a migrations with the name "%s" because it is a reserved number by doctrine migrations' . PHP_EOL .
'It\'s used to revert all migrations including the first one.',
$version
));
}
$migrations[$version] = sprintf('%s\\%s', $namespace, $className);
}
return $migrations;
}
/**
* Return callable for files basename uasort
*
* @return callable
*/
protected function getFileSortCallback()
{
return function ($a, $b) {
return (basename($a) < basename($b)) ? -1 : 1;
};
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Doctrine\DBAL\Migrations\Finder;
/**
* A MigrationFinderInterface implementation that uses `glob` and some special file and
* class names to load migrations from a directory.
*
* The migrations are expected to reside in files with the filename
* `VersionYYYYMMDDHHMMSS.php`. Each file should contain one class named
* `VersionYYYYMMDDHHMMSS`.
*
* @since 1.0.0-alpha3
*/
final class GlobFinder extends AbstractFinder
{
/**
* {@inheritdoc}
*/
public function findMigrations($directory, $namespace = null)
{
$dir = $this->getRealPath($directory);
$files = glob(rtrim($dir, '/') . '/Version*.php');
return $this->loadMigrations($files, $namespace);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Doctrine\DBAL\Migrations\Finder;
/**
* A MigrationDeepFinderInterface is a MigrationFinderInterface, which locates
* migrations not only in a directory itself, but in subdirectories of this directory,
* too.
*/
interface MigrationDeepFinderInterface extends MigrationFinderInterface
{
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Doctrine\DBAL\Migrations\Finder;
/**
* MigrationFinderInterface implementations locate migrations (classes that extend
* `Doctrine\DBAL\Migrations\AbstractMigration`) in a directory.
*
* @since 1.0.0-alpha3
*/
interface MigrationFinderInterface
{
/**
* Find all the migrations in a directory for the given path and namespace.
*
* @param string $directory The directory in which to look for migrations
* @param string|null $namespace The namespace of the classes to load
* @throws \InvalidArgumentException if the directory does not exist
* @return string[] An array of class names that were found with the version
* as keys.
*/
public function findMigrations($directory, $namespace = null);
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Doctrine\DBAL\Migrations\Finder;
/**
* A MigrationFinderInterface implementation that uses a RegexIterator along with a
* RecursiveDirectoryIterator.
*
* @since 1.0.0-alpha3
*/
final class RecursiveRegexFinder extends AbstractFinder implements MigrationDeepFinderInterface
{
/**
* {@inheritdoc}
*/
public function findMigrations($directory, $namespace = null)
{
$dir = $this->getRealPath($directory);
return $this->loadMigrations($this->getMatches($this->createIterator($dir)), $namespace);
}
/**
* Create a recursive iterator to find all the migrations in the subdirectories.
* @param string $dir
* @return \RegexIterator
*/
private function createIterator($dir)
{
return new \RegexIterator(
new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
\RecursiveIteratorIterator::LEAVES_ONLY
),
$this->getPattern(),
\RegexIterator::GET_MATCH
);
}
private function getPattern()
{
return sprintf('#^.+\\%sVersion[^\\%s]{1,255}\\.php$#i', DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR);
}
/**
* Transform the recursiveIterator result array of array into the expected array of migration file
* @param iterable $iteratorFilesMatch
* @return array
*/
private function getMatches($iteratorFilesMatch)
{
$files = [];
foreach ($iteratorFilesMatch as $file) {
$files[] = $file[0];
}
return $files;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Doctrine\DBAL\Migrations;
/**
* Exception to be thrown in the down() methods of migrations that signifies it
* is an irreversible migration and stops execution.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan H. Wage <jonwage@gmail.com>
*/
class IrreversibleMigrationException extends \Exception
{
}

View File

@@ -0,0 +1,201 @@
<?php
namespace Doctrine\DBAL\Migrations;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Doctrine\DBAL\Migrations\Event\MigrationsEventArgs;
use const COUNT_RECURSIVE;
/**
* Class for running migrations to the current version or a manually specified version.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan H. Wage <jonwage@gmail.com>
*/
class Migration
{
/**
* The OutputWriter object instance used for outputting information
*
* @var OutputWriter
*/
private $outputWriter;
/**
* @var Configuration
*/
private $configuration;
/**
* @var boolean
*/
private $noMigrationException;
/**
* Construct a Migration instance
*
* @param Configuration $configuration A migration Configuration instance
*/
public function __construct(Configuration $configuration)
{
$this->configuration = $configuration;
$this->outputWriter = $configuration->getOutputWriter();
$this->noMigrationException = false;
}
/**
* Get the array of versions and SQL queries that would be executed for
* each version but do not execute anything.
*
* @param string $to The version to migrate to.
*
* @return array $sql The array of SQL queries.
*/
public function getSql($to = null)
{
return $this->migrate($to, true);
}
/**
* Write a migration SQL file to the given path
*
* @param string $path The path to write the migration SQL file.
* @param string $to The version to migrate to.
*
* @return boolean $written
*/
public function writeSqlFile($path, $to = null)
{
$sql = $this->getSql($to);
$from = $this->configuration->getCurrentVersion();
if ($to === null) {
$to = $this->configuration->getLatestVersion();
}
$direction = $from > $to ? Version::DIRECTION_DOWN : Version::DIRECTION_UP;
$this->outputWriter->write(sprintf("-- Migrating from %s to %s\n", $from, $to));
/*
* Since the configuration object changes during the creation we cannot inject things
* properly, so I had to violate LoD here (so please, let's find a way to solve it on v2).
*/
return $this->configuration->getQueryWriter()
->write($path, $direction, $sql);
}
/**
* @param boolean $noMigrationException Throw an exception or not if no migration is found. Mostly for Continuous Integration.
*/
public function setNoMigrationException($noMigrationException = false)
{
$this->noMigrationException = $noMigrationException;
}
/**
* Run a migration to the current version or the given target version.
*
* @param string $to The version to migrate to.
* @param boolean $dryRun Whether or not to make this a dry run and not execute anything.
* @param boolean $timeAllQueries Measuring or not the execution time of each SQL query.
* @param callable|null $confirm A callback to confirm whether the migrations should be executed.
*
* @return array An array of migration sql statements. This will be empty if the the $confirm callback declines to execute the migration
*
* @throws MigrationException
*/
public function migrate($to = null, $dryRun = false, $timeAllQueries = false, callable $confirm = null)
{
/**
* If no version to migrate to is given we default to the last available one.
*/
if ($to === null) {
$to = $this->configuration->getLatestVersion();
}
$from = (string) $this->configuration->getCurrentVersion();
$to = (string) $to;
/**
* Throw an error if we can't find the migration to migrate to in the registered
* migrations.
*/
$migrations = $this->configuration->getMigrations();
if ( ! isset($migrations[$to]) && $to > 0) {
throw MigrationException::unknownMigrationVersion($to);
}
$direction = $from > $to ? Version::DIRECTION_DOWN : Version::DIRECTION_UP;
$migrationsToExecute = $this->configuration->getMigrationsToExecute($direction, $to);
/**
* If
* there are no migrations to execute
* and there are migrations,
* and the migration from and to are the same
* means we are already at the destination return an empty array()
* to signify that there is nothing left to do.
*/
if ($from === $to && empty($migrationsToExecute) && ! empty($migrations)) {
return $this->noMigrations();
}
if ( ! $dryRun && false === $this->migrationsCanExecute($confirm)) {
return [];
}
$output = $dryRun ? 'Executing dry run of migration' : 'Migrating';
$output .= ' <info>%s</info> to <comment>%s</comment> from <comment>%s</comment>';
$this->outputWriter->write(sprintf($output, $direction, $to, $from));
/**
* If there are no migrations to execute throw an exception.
*/
if (empty($migrationsToExecute) && ! $this->noMigrationException) {
throw MigrationException::noMigrationsToExecute();
} elseif (empty($migrationsToExecute)) {
return $this->noMigrations();
}
$this->configuration->dispatchEvent(
Events::onMigrationsMigrating,
new MigrationsEventArgs($this->configuration, $direction, $dryRun)
);
$sql = [];
$time = 0;
foreach ($migrationsToExecute as $version) {
$versionSql = $version->execute($direction, $dryRun, $timeAllQueries);
$sql[$version->getVersion()] = $versionSql;
$time += $version->getTime();
}
$this->configuration->dispatchEvent(
Events::onMigrationsMigrated,
new MigrationsEventArgs($this->configuration, $direction, $dryRun)
);
$this->outputWriter->write("\n <comment>------------------------</comment>\n");
$this->outputWriter->write(sprintf(" <info>++</info> finished in %ss", $time));
$this->outputWriter->write(sprintf(" <info>++</info> %s migrations executed", count($migrationsToExecute)));
$this->outputWriter->write(sprintf(" <info>++</info> %s sql queries", count($sql, COUNT_RECURSIVE) - count($sql)));
return $sql;
}
private function noMigrations() : array
{
$this->outputWriter->write('<comment>No migrations to execute.</comment>');
return [];
}
private function migrationsCanExecute(callable $confirm = null) : bool
{
return null === $confirm ? true : (bool) $confirm();
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Doctrine\DBAL\Migrations;
use \Doctrine\DBAL\Migrations\Finder\MigrationFinderInterface;
/**
* Class for Migrations specific exceptions
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan H. Wage <jonwage@gmail.com>
*/
class MigrationException extends \Exception
{
public static function migrationsNamespaceRequired()
{
return new self('Migrations namespace must be configured in order to use Doctrine migrations.', 2);
}
public static function migrationsDirectoryRequired()
{
return new self('Migrations directory must be configured in order to use Doctrine migrations.', 3);
}
public static function noMigrationsToExecute()
{
return new self('Could not find any migrations to execute.', 4);
}
public static function unknownMigrationVersion($version)
{
return new self(sprintf('Could not find migration version %s', $version), 5);
}
public static function alreadyAtVersion($version)
{
return new self(sprintf('Database is already at version %s', $version), 6);
}
public static function duplicateMigrationVersion($version, $class)
{
return new self(sprintf('Migration version %s already registered with class %s', $version, $class), 7);
}
public static function configurationFileAlreadyLoaded()
{
return new self(sprintf('Migrations configuration file already loaded'), 8);
}
public static function yamlConfigurationNotAvailable() : self
{
return new self('Unable to load yaml configuration files, please `composer require symfony/yaml` load yaml configuration files');
}
public static function configurationIncompatibleWithFinder(
$configurationParameterName,
MigrationFinderInterface $finder
) {
return new self(
sprintf(
'Configuration-parameter "%s" cannot be used with finder of type "%s"',
$configurationParameterName,
get_class($finder)
),
9
);
}
public static function configurationNotValid($msg)
{
return new self($msg, 10);
}
/**
* @param string $migrationClass
* @param string $migrationNamespace
* @return MigrationException
*/
public static function migrationClassNotFound($migrationClass, $migrationNamespace)
{
return new self(
sprintf(
'Migration class "%s" was not found. Is it placed in "%s" namespace?',
$migrationClass,
$migrationNamespace
)
);
}
/**
* @param string $migrationClass
* @return MigrationException
*/
public static function migrationNotConvertibleToSql($migrationClass)
{
return new self(
sprintf(
'Migration class "%s" contains a prepared statement.
Unfortunately there is no cross platform way of outputing it as an sql string.
Do you want to write a PR for it ?',
$migrationClass
)
);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Doctrine\DBAL\Migrations;
class MigrationsVersion
{
private static $version = 'v1.8.0';
public static function VERSION()
{
$gitversion = '@git-version@';
if (self::isACustomPharBuild($gitversion)) {
return $gitversion;
}
return self::$version;
}
/**
* @param string $gitversion
* @return bool
*
* Check if doctrine migration is installed by composer or
* in a modified (not tagged) phar version.
*/
private static function isACustomPharBuild($gitversion)
{
return $gitversion !== '@' . 'git-version@';
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Doctrine\DBAL\Migrations;
/**
* Simple class for outputting information from migrations.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan H. Wage <jonwage@gmail.com>
*/
class OutputWriter
{
private $closure;
public function __construct(\Closure $closure = null)
{
if ($closure === null) {
$closure = function ($message) {
};
}
$this->closure = $closure;
}
/**
* Write output using the configured closure.
*
* @param string $message The message to write.
*/
public function write($message)
{
$closure = $this->closure;
$closure($message);
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Doctrine\DBAL\Migrations\Provider;
use Doctrine\DBAL\Schema\Schema;
use ProxyManager\Configuration;
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
use ProxyManager\Proxy\LazyLoadingInterface;
class LazySchemaDiffProvider implements SchemaDiffProviderInterface
{
/** @var LazyLoadingValueHolderFactory */
private $proxyFactory;
/** @var SchemaDiffProviderInterface */
private $originalSchemaManipulator;
public function __construct(LazyLoadingValueHolderFactory $proxyFactory, SchemaDiffProviderInterface $originalSchemaManipulator)
{
$this->proxyFactory = $proxyFactory;
$this->originalSchemaManipulator = $originalSchemaManipulator;
}
public static function fromDefaultProxyFacyoryConfiguration(SchemaDiffProviderInterface $originalSchemaManipulator)
{
$message = 'Function %s::fromDefaultProxyFacyoryConfiguration() deprecated due to typo.'
. 'Use %s::fromDefaultProxyFactoryConfiguration() instead';
trigger_error(
sprintf($message, self::class),
E_USER_DEPRECATED
);
return self::fromDefaultProxyFactoryConfiguration($originalSchemaManipulator);
}
public static function fromDefaultProxyFactoryConfiguration(SchemaDiffProviderInterface $originalSchemaManipulator)
{
$proxyConfig = new Configuration();
$proxyConfig->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
$proxyFactory = new LazyLoadingValueHolderFactory($proxyConfig);
return new LazySchemaDiffProvider($proxyFactory, $originalSchemaManipulator);
}
/**
* @return Schema
*/
public function createFromSchema()
{
$originalSchemaManipulator = $this->originalSchemaManipulator;
return $this->proxyFactory->createProxy(
Schema::class,
function (& $wrappedObject, $proxy, $method, array $parameters, & $initializer) use ($originalSchemaManipulator) {
$initializer = null;
$wrappedObject = $originalSchemaManipulator->createFromSchema();
return true;
}
);
}
/**
* @param Schema $fromSchema
* @return Schema
*/
public function createToSchema(Schema $fromSchema)
{
$originalSchemaManipulator = $this->originalSchemaManipulator;
if ($fromSchema instanceof LazyLoadingInterface && ! $fromSchema->isProxyInitialized()) {
return $this->proxyFactory->createProxy(
Schema::class,
function (& $wrappedObject, $proxy, $method, array $parameters, & $initializer) use ($originalSchemaManipulator, $fromSchema) {
$initializer = null;
$wrappedObject = $originalSchemaManipulator->createToSchema($fromSchema);
return true;
}
);
}
return $this->originalSchemaManipulator->createToSchema($fromSchema);
}
/**
* @param Schema $fromSchema
* @param Schema $toSchema
*
* @return string[]
*/
public function getSqlDiffToMigrate(Schema $fromSchema, Schema $toSchema)
{
if ($toSchema instanceof LazyLoadingInterface
&& ! $toSchema->isProxyInitialized()) {
return [];
}
return $this->originalSchemaManipulator->getSqlDiffToMigrate($fromSchema, $toSchema);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Doctrine\DBAL\Migrations\Provider;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\SchemaTool;
/**
* A schema provider that uses the doctrine ORM to generate schemas.
*
* @since 1.0.0-alpha3
*/
final class OrmSchemaProvider implements SchemaProviderInterface
{
/**
* @var EntityManagerInterface
*/
private $entityManager;
public function __construct($em)
{
if ( ! $this->isEntityManager($em)) {
throw new \InvalidArgumentException(sprintf(
'$em is not a valid Doctrine ORM Entity Manager, got "%s"',
is_object($em) ? get_class($em) : gettype($em)
));
}
$this->entityManager = $em;
}
/**
* {@inheritdoc}
*/
public function createSchema()
{
$metadata = $this->entityManager->getMetadataFactory()->getAllMetadata();
if (empty($metadata)) {
throw new \UnexpectedValueException('No mapping information to process');
}
$tool = new SchemaTool($this->entityManager);
return $tool->getSchemaFromMetadata($metadata);
}
/**
* Doctrine's EntityManagerInterface was introduced in version 2.4, since this
* library allows those older version we need to be able to check for those
* old ORM versions. Hence the helper method.
*
* No need to check to see if EntityManagerInterface exists first here, PHP
* doesn't care.
*
* @param mixed $manager Hopefully an entity manager, but it may be anything
* @return boolean
*/
private function isEntityManager($manager)
{
return $manager instanceof EntityManagerInterface || $manager instanceof EntityManager;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Doctrine\DBAL\Migrations\Provider;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\Schema;
class SchemaDiffProvider implements SchemaDiffProviderInterface
{
/** @var AbstractPlatform */
private $platform;
/** @var AbstractSchemaManager */
private $schemaManager;
public function __construct(AbstractSchemaManager $schemaManager, AbstractPlatform $platform)
{
$this->schemaManager = $schemaManager;
$this->platform = $platform;
}
/**
* @return Schema
*/
public function createFromSchema()
{
return $this->schemaManager->createSchema();
}
/**
* @param Schema $fromSchema
* @return Schema
*/
public function createToSchema(Schema $fromSchema)
{
return clone $fromSchema;
}
/**
* @param Schema $fromSchema
* @param Schema $toSchema
* @return string[]
*/
public function getSqlDiffToMigrate(Schema $fromSchema, Schema $toSchema)
{
return $fromSchema->getMigrateToSql($toSchema, $this->platform);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Doctrine\DBAL\Migrations\Provider;
use Doctrine\DBAL\Schema\Schema;
/**
* Generates `Schema` objects to be passed to the migrations class.
*
* @since 1.3
*/
interface SchemaDiffProviderInterface
{
/**
* Create the schema that represent the current state of the database.
*
* @return Schema
*/
public function createFromSchema();
/**
* Create the schema that will represent the future state of the database
*
* @param Schema $fromSchema
* @return Schema
*/
public function createToSchema(Schema $fromSchema);
/**
* Return an array of sql statement that migrate the database state from the
* fromSchema to the toSchema.
*
* @param Schema $fromSchema
* @param Schema $toSchema
*
* @return string[]
*/
public function getSqlDiffToMigrate(Schema $fromSchema, Schema $toSchema);
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Doctrine\DBAL\Migrations\Provider;
/**
* Generates `Schema` objects for the diff command. A schema provider should
* return the schema to which the database should be migrated.
*
* @since 1.0.0-alpha3
*/
interface SchemaProviderInterface
{
/**
* Create the schema to which the database should be migrated.
*
* @return \Doctrine\DBAL\Schema\Schema
*/
public function createSchema();
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Doctrine\DBAL\Migrations\Provider;
use Doctrine\DBAL\Schema\Schema;
/**
* A schemea provider implementation that just returns the schema its given.
*
* @since 1.0.0-alpha3
*/
final class StubSchemaProvider implements SchemaProviderInterface
{
/**
* @var Schema
*/
private $toSchema;
public function __construct(Schema $schema)
{
$this->toSchema = $schema;
}
/**
* {@inheritdoc}
*/
public function createSchema()
{
return $this->toSchema;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Migrations;
/**
* @since 1.6.0
* @author Luís Cobucci <lcobucci@gmail.com>
*/
interface QueryWriter
{
/**
* @param string $path
* @param string $direction
* @param array $queriesByVersion
* @return bool
*/
public function write(string $path, string $direction, array $queriesByVersion) : bool;
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Doctrine\DBAL\Migrations;
class SkipMigrationException extends MigrationException
{
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Doctrine\DBAL\Migrations;
use Doctrine\DBAL\Exception\InvalidArgumentException;
/**
* @deprecated
*
* @see \Doctrine\DBAL\Migrations\FileQueryWriter
*/
class SqlFileWriter
{
private $migrationsColumnName;
private $migrationsTableName;
private $destPath;
/** @var null|OutputWriter */
private $outputWriter;
/**
* @param string $migrationsColumnName
* @param string $migrationsTableName
* @param string $destPath
* @param \Doctrine\DBAL\Migrations\OutputWriter $outputWriter
*/
public function __construct(
$migrationsColumnName,
$migrationsTableName,
$destPath,
OutputWriter $outputWriter = null
) {
if (empty($migrationsColumnName)) {
$this->throwInvalidArgumentException('Migrations column name cannot be empty.');
}
if (empty($migrationsTableName)) {
$this->throwInvalidArgumentException('Migrations table name cannot be empty.');
}
if (empty($destPath)) {
$this->throwInvalidArgumentException('Destination file must be specified.');
}
$this->migrationsColumnName = $migrationsColumnName;
$this->migrationsTableName = $migrationsTableName;
$this->destPath = $destPath;
$this->outputWriter = $outputWriter;
}
/**
* @param array $queriesByVersion array Keys are versions and values are arrays of SQL queries (they must be castable to string)
* @param string $direction
* @return int|bool
*/
public function write(array $queriesByVersion, $direction)
{
$path = $this->buildMigrationFilePath();
$string = $this->buildMigrationFile($queriesByVersion, $direction);
if ($this->outputWriter) {
$this->outputWriter->write("\n" . sprintf('Writing migration file to "<info>%s</info>"', $path));
}
return file_put_contents($path, $string);
}
private function buildMigrationFile(array $queriesByVersion, string $direction) : string
{
$string = sprintf("-- Doctrine Migration File Generated on %s\n", date('Y-m-d H:i:s'));
foreach ($queriesByVersion as $version => $queries) {
$string .= "\n-- Version " . $version . "\n";
foreach ($queries as $query) {
$string .= $query . ";\n";
}
$string .= $this->getVersionUpdateQuery($version, $direction);
}
return $string;
}
private function getVersionUpdateQuery(string $version, string $direction) : string
{
if ($direction === Version::DIRECTION_DOWN) {
$query = "DELETE FROM %s WHERE %s = '%s';\n";
} else {
$query = "INSERT INTO %s (%s) VALUES ('%s');\n";
}
return sprintf($query, $this->migrationsTableName, $this->migrationsColumnName, $version);
}
private function buildMigrationFilePath() : string
{
$path = $this->destPath;
if (is_dir($path)) {
$path = realpath($path);
$path = $path . '/doctrine_migration_' . date('YmdHis') . '.sql';
}
return $path;
}
protected function throwInvalidArgumentException($message)
{
throw new InvalidArgumentException($message);
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console\Command;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Doctrine\DBAL\Migrations\Configuration\Connection\Loader\ArrayConnectionConfigurationLoader;
use Doctrine\DBAL\Migrations\Configuration\Connection\Loader\ConnectionConfigurationLoader;
use Doctrine\DBAL\Migrations\Configuration\Connection\Loader\ConnectionHelperLoader;
use Doctrine\DBAL\Migrations\Configuration\Connection\Loader\ConnectionConfigurationChainLoader;
use Doctrine\DBAL\Migrations\OutputWriter;
use Doctrine\DBAL\Migrations\Tools\Console\Helper\ConfigurationHelper;
use Doctrine\DBAL\Migrations\Tools\Console\Helper\ConfigurationHelperInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
/**
* CLI Command for adding and deleting migration versions from the version table.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*/
abstract class AbstractCommand extends Command
{
/**
* The configuration property only contains the configuration injected by the setter.
*
* @var Configuration
*/
private $configuration;
/**
* The migrationConfiguration property contains the configuration
* created taking into account the command line options.
*
* @var Configuration
*/
private $migrationConfiguration;
/**
* @var OutputWriter
*/
private $outputWriter;
/**
* @var \Doctrine\DBAL\Connection
*/
private $connection;
protected function configure()
{
$this->addOption('configuration', null, InputOption::VALUE_OPTIONAL, 'The path to a migrations configuration file.');
$this->addOption('db-configuration', null, InputOption::VALUE_OPTIONAL, 'The path to a database connection configuration file.');
}
protected function outputHeader(Configuration $configuration, OutputInterface $output)
{
$name = $configuration->getName();
$name = $name ? $name : 'Doctrine Database Migrations';
$name = str_repeat(' ', 20) . $name . str_repeat(' ', 20);
$output->writeln('<question>' . str_repeat(' ', strlen($name)) . '</question>');
$output->writeln('<question>' . $name . '</question>');
$output->writeln('<question>' . str_repeat(' ', strlen($name)) . '</question>');
$output->writeln('');
}
public function setMigrationConfiguration(Configuration $config)
{
$this->configuration = $config;
}
/**
* When any (config) command line option is passed to the migration the migrationConfiguration
* property is set with the new generated configuration.
* If no (config) option is passed the migrationConfiguration property is set to the value
* of the configuration one (if any).
* Else a new configuration is created and assigned to the migrationConfiguration property.
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @return Configuration
*/
protected function getMigrationConfiguration(InputInterface $input, OutputInterface $output)
{
if ( ! $this->migrationConfiguration) {
if ($this->getHelperSet()->has('configuration')
&& $this->getHelperSet()->get('configuration') instanceof ConfigurationHelperInterface) {
$configHelper = $this->getHelperSet()->get('configuration');
} else {
$configHelper = new ConfigurationHelper($this->getConnection($input), $this->configuration);
}
$this->migrationConfiguration = $configHelper->getMigrationConfig($input, $this->getOutputWriter($output));
}
return $this->migrationConfiguration;
}
/**
* @param string $question
* @param InputInterface $input
* @param OutputInterface $output
* @return mixed
*/
protected function askConfirmation($question, InputInterface $input, OutputInterface $output)
{
return $this->getHelper('question')->ask($input, $output, new ConfirmationQuestion($question));
}
/**
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return \Doctrine\DBAL\Migrations\OutputWriter
*/
private function getOutputWriter(OutputInterface $output)
{
if ( ! $this->outputWriter) {
$this->outputWriter = new OutputWriter(function ($message) use ($output) {
return $output->writeln($message);
});
}
return $this->outputWriter;
}
/**
* @param \Symfony\Component\Console\Input\InputInterface $input
*
* @return \Doctrine\DBAL\Connection
* @throws \Doctrine\DBAL\DBALException
*/
private function getConnection(InputInterface $input)
{
if ($this->connection) {
return $this->connection;
}
$chainLoader = new ConnectionConfigurationChainLoader(
[
new ArrayConnectionConfigurationLoader($input->getOption('db-configuration')),
new ArrayConnectionConfigurationLoader('migrations-db.php'),
new ConnectionHelperLoader($this->getHelperSet(), 'connection'),
new ConnectionConfigurationLoader($this->configuration),
]
);
$connection = $chainLoader->chosen();
if ($connection) {
return $this->connection = $connection;
}
throw new \InvalidArgumentException('You have to specify a --db-configuration file or pass a Database Connection as a dependency to the Migrations.');
}
}

View File

@@ -0,0 +1,182 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console\Command;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Doctrine\DBAL\Version as DbalVersion;
use Doctrine\DBAL\Migrations\Provider\SchemaProviderInterface;
use Doctrine\DBAL\Migrations\Provider\OrmSchemaProvider;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
/**
* Command for generate migration classes by comparing your current database schema
* to your mapping information.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*/
class DiffCommand extends GenerateCommand
{
/**
* @var SchemaProviderInterface
*/
protected $schemaProvider;
public function __construct(SchemaProviderInterface $schemaProvider = null)
{
$this->schemaProvider = $schemaProvider;
parent::__construct();
}
protected function configure()
{
parent::configure();
$this
->setName('migrations:diff')
->setDescription('Generate a migration by comparing your current database to your mapping information.')
->setHelp(<<<EOT
The <info>%command.name%</info> command generates a migration by comparing your current database to your mapping information:
<info>%command.full_name%</info>
You can optionally specify a <comment>--editor-cmd</comment> option to open the generated file in your favorite editor:
<info>%command.full_name% --editor-cmd=mate</info>
EOT
)
->addOption('filter-expression', null, InputOption::VALUE_OPTIONAL, 'Tables which are filtered by Regular Expression.')
->addOption('formatted', null, InputOption::VALUE_NONE, 'Format the generated SQL.')
->addOption('line-length', null, InputOption::VALUE_OPTIONAL, 'Max line length of unformatted lines.', 120)
;
}
public function execute(InputInterface $input, OutputInterface $output)
{
$isDbalOld = (DbalVersion::compare('2.2.0') > 0);
$configuration = $this->getMigrationConfiguration($input, $output);
$this->loadCustomTemplate($configuration, $output);
$conn = $configuration->getConnection();
$platform = $conn->getDatabasePlatform();
if ($filterExpr = $input->getOption('filter-expression')) {
if ($isDbalOld) {
throw new \InvalidArgumentException('The "--filter-expression" option can only be used as of Doctrine DBAL 2.2');
}
$conn->getConfiguration()
->setFilterSchemaAssetsExpression($filterExpr);
}
$fromSchema = $conn->getSchemaManager()->createSchema();
$toSchema = $this->getSchemaProvider()->createSchema();
//Not using value from options, because filters can be set from config.yml
if ( ! $isDbalOld && $filterExpr = $conn->getConfiguration()->getFilterSchemaAssetsExpression()) {
foreach ($toSchema->getTables() as $table) {
$tableName = $table->getName();
if ( ! preg_match($filterExpr, $this->resolveTableName($tableName))) {
$toSchema->dropTable($tableName);
}
}
}
$up = $this->buildCodeFromSql(
$configuration,
$fromSchema->getMigrateToSql($toSchema, $platform),
$input->getOption('formatted'),
$input->getOption('line-length')
);
$down = $this->buildCodeFromSql(
$configuration,
$fromSchema->getMigrateFromSql($toSchema, $platform),
$input->getOption('formatted'),
$input->getOption('line-length')
);
if ( ! $up && ! $down) {
$output->writeln('No changes detected in your mapping information.');
return;
}
$version = $configuration->generateVersionNumber();
$path = $this->generateMigration($configuration, $input, $version, $up, $down);
$output->writeln(sprintf('Generated new migration class to "<info>%s</info>" from schema differences.', $path));
$output->writeln(file_get_contents($path), OutputInterface::VERBOSITY_VERBOSE);
}
private function buildCodeFromSql(Configuration $configuration, array $sql, $formatted = false, $lineLength = 120)
{
$currentPlatform = $configuration->getConnection()->getDatabasePlatform()->getName();
$code = [];
foreach ($sql as $query) {
if (stripos($query, $configuration->getMigrationsTableName()) !== false) {
continue;
}
if ($formatted) {
if ( ! class_exists('\SqlFormatter')) {
throw new \InvalidArgumentException(
'The "--formatted" option can only be used if the sql formatter is installed.' .
'Please run "composer require jdorn/sql-formatter".'
);
}
$maxLength = $lineLength - 18 - 8; // max - php code length - indentation
if (strlen($query) > $maxLength) {
$query = \SqlFormatter::format($query, false);
}
}
$code[] = sprintf("\$this->addSql(%s);", var_export($query, true));
}
if ( ! empty($code)) {
array_unshift(
$code,
sprintf(
"\$this->abortIf(\$this->connection->getDatabasePlatform()->getName() !== %s, %s);",
var_export($currentPlatform, true),
var_export(sprintf("Migration can only be executed safely on '%s'.", $currentPlatform), true)
),
""
);
}
return implode("\n", $code);
}
private function getSchemaProvider()
{
if ( ! $this->schemaProvider) {
$this->schemaProvider = new OrmSchemaProvider($this->getHelper('entityManager')->getEntityManager());
}
return $this->schemaProvider;
}
/**
* Resolve a table name from its fully qualified name. The `$name` argument
* comes from Doctrine\DBAL\Schema\Table#getName which can sometimes return
* a namespaced name with the form `{namespace}.{tableName}`. This extracts
* the table name from that.
*
* @param string $name
* @return string
*/
private function resolveTableName($name)
{
$pos = strpos($name, '.');
return false === $pos ? $name : substr($name, $pos + 1);
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
/**
* Command for executing single migrations up or down manually.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*/
class ExecuteCommand extends AbstractCommand
{
protected function configure()
{
$this
->setName('migrations:execute')
->setDescription('Execute a single migration version up or down manually.')
->addArgument('version', InputArgument::REQUIRED, 'The version to execute.', null)
->addOption('write-sql', null, InputOption::VALUE_NONE, 'The path to output the migration SQL file instead of executing it.')
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Execute the migration as a dry run.')
->addOption('up', null, InputOption::VALUE_NONE, 'Execute the migration up.')
->addOption('down', null, InputOption::VALUE_NONE, 'Execute the migration down.')
->addOption('query-time', null, InputOption::VALUE_NONE, 'Time all the queries individually.')
->setHelp(<<<EOT
The <info>%command.name%</info> command executes a single migration version up or down manually:
<info>%command.full_name% YYYYMMDDHHMMSS</info>
If no <comment>--up</comment> or <comment>--down</comment> option is specified it defaults to up:
<info>%command.full_name% YYYYMMDDHHMMSS --down</info>
You can also execute the migration as a <comment>--dry-run</comment>:
<info>%command.full_name% YYYYMMDDHHMMSS --dry-run</info>
You can output the would be executed SQL statements to a file with <comment>--write-sql</comment>:
<info>%command.full_name% YYYYMMDDHHMMSS --write-sql</info>
Or you can also execute the migration without a warning message which you need to interact with:
<info>%command.full_name% YYYYMMDDHHMMSS --no-interaction</info>
EOT
);
parent::configure();
}
public function execute(InputInterface $input, OutputInterface $output)
{
$version = $input->getArgument('version');
$direction = $input->getOption('down') ? 'down' : 'up';
$configuration = $this->getMigrationConfiguration($input, $output);
$version = $configuration->getVersion($version);
$timeAllqueries = $input->getOption('query-time');
if ($path = $input->getOption('write-sql')) {
$path = is_bool($path) ? getcwd() : $path;
$version->writeSqlFile($path, $direction);
} else {
if ($input->isInteractive()) {
$question = 'WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)';
$execute = $this->askConfirmation($question, $input, $output);
} else {
$execute = true;
}
if ($execute) {
$version->execute($direction, (boolean) $input->getOption('dry-run'), $timeAllqueries);
} else {
$output->writeln('<error>Migration cancelled!</error>');
}
}
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console\Command;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Doctrine\DBAL\Migrations\Tools\Console\Helper\MigrationDirectoryHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
/**
* Command for generating new blank migration classes
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*/
class GenerateCommand extends AbstractCommand
{
private static $_template =
'<?php declare(strict_types=1);
namespace <namespace>;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version<version> extends AbstractMigration
{
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
<up>
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
<down>
}
}
';
private $instanceTemplate;
protected function configure()
{
$this
->setName('migrations:generate')
->setDescription('Generate a blank migration class.')
->addOption('editor-cmd', null, InputOption::VALUE_OPTIONAL, 'Open file with this command upon creation.')
->setHelp(<<<EOT
The <info>%command.name%</info> command generates a blank migration class:
<info>%command.full_name%</info>
You can optionally specify a <comment>--editor-cmd</comment> option to open the generated file in your favorite editor:
<info>%command.full_name% --editor-cmd=mate</info>
EOT
);
parent::configure();
}
public function execute(InputInterface $input, OutputInterface $output)
{
$configuration = $this->getMigrationConfiguration($input, $output);
$this->loadCustomTemplate($configuration, $output);
$version = $configuration->generateVersionNumber();
$path = $this->generateMigration($configuration, $input, $version);
$output->writeln(sprintf('Generated new migration class to "<info>%s</info>"', $path));
}
protected function getTemplate()
{
if ($this->instanceTemplate === null) {
$this->instanceTemplate = self::$_template;
}
return $this->instanceTemplate;
}
protected function generateMigration(Configuration $configuration, InputInterface $input, $version, $up = null, $down = null)
{
$placeHolders = [
'<namespace>',
'<version>',
'<up>',
'<down>',
];
$replacements = [
$configuration->getMigrationsNamespace(),
$version,
$up ? " " . implode("\n ", explode("\n", $up)) : null,
$down ? " " . implode("\n ", explode("\n", $down)) : null,
];
$code = str_replace($placeHolders, $replacements, $this->getTemplate());
$code = preg_replace('/^ +$/m', '', $code);
$directoryHelper = new MigrationDirectoryHelper($configuration);
$dir = $directoryHelper->getMigrationDirectory();
$path = $dir . '/Version' . $version . '.php';
file_put_contents($path, $code);
if ($editorCmd = $input->getOption('editor-cmd')) {
proc_open($editorCmd . ' ' . escapeshellarg($path), [], $pipes);
}
return $path;
}
protected function loadCustomTemplate(Configuration $configuration, OutputInterface $output) : void
{
$customTemplate = $configuration->getCustomTemplate();
if ($customTemplate === null) {
return;
}
if ( ! is_file($customTemplate) || ! is_readable($customTemplate)) {
throw new \InvalidArgumentException(
'The specified template "' . $customTemplate . '" cannot be found or is not readable.'
);
}
$content = file_get_contents($customTemplate);
if ($content === false) {
throw new \InvalidArgumentException('The specified template "' . $customTemplate . '" could not be read.');
}
$output->writeln(sprintf('Using custom migration template "<info>%s</info>"', $customTemplate));
$this->instanceTemplate = $content;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Outputs the latest version number.
*
* @author Kris Wallsmith <kris.wallsmith@gmail.com>
*/
class LatestCommand extends AbstractCommand
{
protected function configure()
{
$this
->setName('migrations:latest')
->setDescription('Outputs the latest version number')
;
parent::configure();
}
public function execute(InputInterface $input, OutputInterface $output)
{
$configuration = $this->getMigrationConfiguration($input, $output);
$output->writeln($configuration->getLatestVersion());
}
}

View File

@@ -0,0 +1,198 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console\Command;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Doctrine\DBAL\Migrations\Migration;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
/**
* Command for executing a migration to a specified version or the latest available version.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*/
class MigrateCommand extends AbstractCommand
{
protected function configure()
{
$this
->setName('migrations:migrate')
->setDescription('Execute a migration to a specified version or the latest available version.')
->addArgument('version', InputArgument::OPTIONAL, 'The version number (YYYYMMDDHHMMSS) or alias (first, prev, next, latest) to migrate to.', 'latest')
->addOption('write-sql', null, InputOption::VALUE_OPTIONAL, 'The path to output the migration SQL file instead of executing it. Default to current working directory.')
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Execute the migration as a dry run.')
->addOption('query-time', null, InputOption::VALUE_NONE, 'Time all the queries individually.')
->addOption('allow-no-migration', null, InputOption::VALUE_NONE, 'Don\'t throw an exception if no migration is available (CI).')
->setHelp(<<<EOT
The <info>%command.name%</info> command executes a migration to a specified version or the latest available version:
<info>%command.full_name%</info>
You can optionally manually specify the version you wish to migrate to:
<info>%command.full_name% YYYYMMDDHHMMSS</info>
You can specify the version you wish to migrate to using an alias:
<info>%command.full_name% prev</info>
<info>These alias are defined : first, latest, prev, current and next</info>
You can specify the version you wish to migrate to using an number against the current version:
<info>%command.full_name% current+3</info>
You can also execute the migration as a <comment>--dry-run</comment>:
<info>%command.full_name% YYYYMMDDHHMMSS --dry-run</info>
You can output the would be executed SQL statements to a file with <comment>--write-sql</comment>:
<info>%command.full_name% YYYYMMDDHHMMSS --write-sql</info>
Or you can also execute the migration without a warning message which you need to interact with:
<info>%command.full_name% --no-interaction</info>
You can also time all the different queries if you wanna know which one is taking so long:
<info>%command.full_name% --query-time</info>
EOT
);
parent::configure();
}
public function execute(InputInterface $input, OutputInterface $output)
{
$configuration = $this->getMigrationConfiguration($input, $output);
$migration = $this->createMigration($configuration);
$this->outputHeader($configuration, $output);
$timeAllqueries = $input->getOption('query-time');
$dryRun = (boolean) $input->getOption('dry-run');
$configuration->setIsDryRun($dryRun);
$executedMigrations = $configuration->getMigratedVersions();
$availableMigrations = $configuration->getAvailableVersions();
$version = $this->getVersionNameFromAlias($input->getArgument('version'), $output, $configuration);
if ($version === false) {
return 1;
}
$executedUnavailableMigrations = array_diff($executedMigrations, $availableMigrations);
if ( ! empty($executedUnavailableMigrations)) {
$output->writeln(sprintf(
'<error>WARNING! You have %s previously executed migrations'
. ' in the database that are not registered migrations.</error>',
count($executedUnavailableMigrations)
));
foreach ($executedUnavailableMigrations as $executedUnavailableMigration) {
$output->writeln(sprintf(
' <comment>>></comment> %s (<comment>%s</comment>)',
$configuration->getDateTime($executedUnavailableMigration),
$executedUnavailableMigration
));
}
$question = 'Are you sure you wish to continue? (y/n)';
if ( ! $this->canExecute($question, $input, $output)) {
$output->writeln('<error>Migration cancelled!</error>');
return 1;
}
}
if ($path = $input->getOption('write-sql')) {
$path = is_bool($path) ? getcwd() : $path;
$migration->writeSqlFile($path, $version);
return 0;
}
$cancelled = false;
$migration->setNoMigrationException($input->getOption('allow-no-migration'));
$result = $migration->migrate($version, $dryRun, $timeAllqueries, function () use ($input, $output, &$cancelled) {
$question = 'WARNING! You are about to execute a database migration'
. ' that could result in schema changes and data loss.'
. ' Are you sure you wish to continue? (y/n)';
$canContinue = $this->canExecute($question, $input, $output);
$cancelled = ! $canContinue;
return $canContinue;
});
if ($cancelled) {
$output->writeln('<error>Migration cancelled!</error>');
return 1;
}
}
/**
* Create a new migration instance to execute the migrations.
*
* @param Configuration $configuration The configuration with which the migrations will be executed
* @return Migration a new migration instance
*/
protected function createMigration(Configuration $configuration)
{
return new Migration($configuration);
}
/**
* @param string $question
* @param InputInterface $input
* @param OutputInterface $output
* @return bool
*/
private function canExecute($question, InputInterface $input, OutputInterface $output)
{
if ($input->isInteractive() && ! $this->askConfirmation($question, $input, $output)) {
return false;
}
return true;
}
/**
* @param string $versionAlias
* @param OutputInterface $output
* @param Configuration $configuration
* @return bool|string
*/
private function getVersionNameFromAlias($versionAlias, OutputInterface $output, Configuration $configuration)
{
$version = $configuration->resolveVersionAlias($versionAlias);
if ($version === null) {
if ($versionAlias == 'prev') {
$output->writeln('<error>Already at first version.</error>');
return false;
}
if ($versionAlias == 'next') {
$output->writeln('<error>Already at latest version.</error>');
return false;
}
if (substr($versionAlias, 0, 7) == 'current') {
$output->writeln('<error>The delta couldn\'t be reached.</error>');
return false;
}
$output->writeln(sprintf(
'<error>Unknown version: %s</error>',
OutputFormatter::escape($versionAlias)
));
return false;
}
return $version;
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console\Command;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Doctrine\DBAL\Migrations\Tools\Console\Helper\MigrationStatusInfosHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
/**
* Command to view the status of a set of migrations.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*/
class StatusCommand extends AbstractCommand
{
protected function configure()
{
$this
->setName('migrations:status')
->setDescription('View the status of a set of migrations.')
->addOption('show-versions', null, InputOption::VALUE_NONE, 'This will display a list of all available migrations and their status')
->setHelp(<<<EOT
The <info>%command.name%</info> command outputs the status of a set of migrations:
<info>%command.full_name%</info>
You can output a list of all available migrations and their status with <comment>--show-versions</comment>:
<info>%command.full_name% --show-versions</info>
EOT
);
parent::configure();
}
public function execute(InputInterface $input, OutputInterface $output)
{
$configuration = $this->getMigrationConfiguration($input, $output);
$infos = new MigrationStatusInfosHelper($configuration);
$output->writeln("\n <info>==</info> Configuration\n");
foreach ($infos->getMigrationsInfos() as $name => $value) {
if ($name == 'New Migrations') {
$value = $value > 0 ? '<question>' . $value . '</question>' : 0;
}
if ($name == 'Executed Unavailable Migrations') {
$value = $value > 0 ? '<error>' . $value . '</error>' : 0;
}
$this->writeStatusInfosLineAligned($output, $name, $value);
}
if ($input->getOption('show-versions')) {
if ($migrations = $configuration->getMigrations()) {
$output->writeln("\n <info>==</info> Available Migration Versions\n");
$this->showVersions($migrations, $configuration, $output);
}
if (count($infos->getExecutedUnavailableMigrations())) {
$output->writeln("\n <info>==</info> Previously Executed Unavailable Migration Versions\n");
foreach ($infos->getExecutedUnavailableMigrations() as $executedUnavailableMigration) {
$output->writeln(' <comment>>></comment> ' . $configuration->getDateTime($executedUnavailableMigration) .
' (<comment>' . $executedUnavailableMigration . '</comment>)');
}
}
}
}
private function writeStatusInfosLineAligned(OutputInterface $output, $title, $value)
{
$output->writeln(' <comment>>></comment> ' . $title . ': ' . str_repeat(' ', 50 - strlen($title)) . $value);
}
private function showVersions($migrations, Configuration $configuration, OutputInterface $output)
{
$migratedVersions = $configuration->getMigratedVersions();
foreach ($migrations as $version) {
$isMigrated = in_array($version->getVersion(), $migratedVersions, true);
$status = $isMigrated ? '<info>migrated</info>' : '<error>not migrated</error>';
$migrationDescription = $version->getMigration()->getDescription()
? str_repeat(' ', 5) . $version->getMigration()->getDescription()
: '';
$formattedVersion = $configuration->getDateTime($version->getVersion());
$output->writeln(' <comment>>></comment> ' . $formattedVersion .
' (<comment>' . $version->getVersion() . '</comment>)' .
str_repeat(' ', max(1, 49 - strlen($formattedVersion) - strlen($version->getVersion()))) .
$status . $migrationDescription);
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Command to show if your schema is up-to-date.
*/
class UpToDateCommand extends AbstractCommand
{
protected function configure()
{
$this
->setName('migrations:up-to-date')
->setDescription('Tells you if your schema is up-to-date.')
->setHelp(<<<EOT
The <info>%command.name%</info> command tells you if your schema is up-to-date:
<info>%command.full_name%</info>
EOT
);
parent::configure();
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int|null
*/
public function execute(InputInterface $input, OutputInterface $output)
{
$configuration = $this->getMigrationConfiguration($input, $output);
$migrations = count($configuration->getMigrations());
$migratedVersions = count($configuration->getMigratedVersions());
$availableMigrations = $migrations - $migratedVersions;
if ($availableMigrations === 0) {
$output->writeln('<comment>Up-to-date! No migrations to execute.</comment>');
return 0;
}
$output->writeln(sprintf(
'<comment>Out-of-date! %u migration%s available to execute.</comment>',
$availableMigrations,
$availableMigrations > 1 ? 's are' : ' is'
));
return 1;
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console\Command;
use Doctrine\DBAL\Migrations\MigrationException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
/**
* Command for manually adding and deleting migration versions from the version table.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*/
class VersionCommand extends AbstractCommand
{
/**
* The Migrations Configuration instance
*
* @var \Doctrine\DBAL\Migrations\Configuration\Configuration
*/
private $configuration;
/**
* Whether or not the versions have to be marked as migrated or not
*
* @var boolean
*/
private $markMigrated;
protected function configure()
{
$this
->setName('migrations:version')
->setDescription('Manually add and delete migration versions from the version table.')
->addArgument('version', InputArgument::OPTIONAL, 'The version to add or delete.', null)
->addOption('add', null, InputOption::VALUE_NONE, 'Add the specified version.')
->addOption('delete', null, InputOption::VALUE_NONE, 'Delete the specified version.')
->addOption('all', null, InputOption::VALUE_NONE, 'Apply to all the versions.')
->addOption('range-from', null, InputOption::VALUE_OPTIONAL, 'Apply from specified version.')
->addOption('range-to', null, InputOption::VALUE_OPTIONAL, 'Apply to specified version.')
->setHelp(<<<EOT
The <info>%command.name%</info> command allows you to manually add, delete or synchronize migration versions from the version table:
<info>%command.full_name% YYYYMMDDHHMMSS --add</info>
If you want to delete a version you can use the <comment>--delete</comment> option:
<info>%command.full_name% YYYYMMDDHHMMSS --delete</info>
If you want to synchronize by adding or deleting all migration versions available in the version table you can use the <comment>--all</comment> option:
<info>%command.full_name% --add --all</info>
<info>%command.full_name% --delete --all</info>
If you want to synchronize by adding or deleting some range of migration versions available in the version table you can use the <comment>--range-from/--range-to</comment> option:
<info>%command.full_name% --add --range-from=YYYYMMDDHHMMSS --range-to=YYYYMMDDHHMMSS</info>
<info>%command.full_name% --delete --range-from=YYYYMMDDHHMMSS --range-to=YYYYMMDDHHMMSS</info>
You can also execute this command without a warning message which you need to interact with:
<info>%command.full_name% --no-interaction</info>
EOT
);
parent::configure();
}
public function execute(InputInterface $input, OutputInterface $output)
{
$this->configuration = $this->getMigrationConfiguration($input, $output);
if ( ! $input->getOption('add') && ! $input->getOption('delete')) {
throw new \InvalidArgumentException('You must specify whether you want to --add or --delete the specified version.');
}
$this->markMigrated = (boolean) $input->getOption('add');
if ($input->isInteractive()) {
$question = 'WARNING! You are about to add, delete or synchronize migration versions from the version table that could result in data lost. Are you sure you wish to continue? (y/n)';
$confirmation = $this->askConfirmation($question, $input, $output);
if ($confirmation) {
$this->markVersions($input);
} else {
$output->writeln('<error>Migration cancelled!</error>');
}
} else {
$this->markVersions($input);
}
}
private function markVersions(InputInterface $input)
{
$affectedVersion = $input->getArgument('version');
$allOption = $input->getOption('all');
$rangeFromOption = $input->getOption('range-from');
$rangeToOption = $input->getOption('range-to');
if ($allOption && ($rangeFromOption !== null || $rangeToOption !== null)) {
throw new \InvalidArgumentException('Options --all and --range-to/--range-from both used. You should use only one of them.');
}
if ($rangeFromOption !== null ^ $rangeToOption !== null) {
throw new \InvalidArgumentException('Options --range-to and --range-from should be used together.');
}
if ($allOption === true) {
$availableVersions = $this->configuration->getAvailableVersions();
foreach ($availableVersions as $version) {
$this->mark($version, true);
}
} elseif ($rangeFromOption !== null && $rangeToOption !== null) {
$availableVersions = $this->configuration->getAvailableVersions();
foreach ($availableVersions as $version) {
if ($version >= $rangeFromOption && $version <= $rangeToOption) {
$this->mark($version, true);
}
}
} else {
$this->mark($affectedVersion);
}
}
private function mark($version, $all = false)
{
if ( ! $this->configuration->hasVersion($version)) {
throw MigrationException::unknownMigrationVersion($version);
}
$version = $this->configuration->getVersion($version);
if ($this->markMigrated && $this->configuration->hasVersionMigrated($version)) {
if ( ! $all) {
throw new \InvalidArgumentException(sprintf('The version "%s" already exists in the version table.', $version));
}
$marked = true;
}
if ( ! $this->markMigrated && ! $this->configuration->hasVersionMigrated($version)) {
if ( ! $all) {
throw new \InvalidArgumentException(sprintf('The version "%s" does not exist in the version table.', $version));
}
$marked = false;
}
if ( ! isset($marked)) {
if ($this->markMigrated) {
$version->markMigrated();
} else {
$version->markNotMigrated();
}
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console;
use Doctrine\DBAL\Migrations\MigrationsVersion;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\HelperSet;
/**
* Handles running the Console Tools inside Symfony Console context.
*/
class ConsoleRunner
{
/**
* Runs console with the given helperset.
*
* @param \Symfony\Component\Console\Helper\HelperSet $helperSet
* @param \Symfony\Component\Console\Command\Command[] $commands
*
* @return void
*/
public static function run(HelperSet $helperSet, $commands = [])
{
$cli = self::createApplication($helperSet, $commands);
$cli->run();
}
/**
* Creates a console application with the given helperset and
* optional commands.
*
* @param \Symfony\Component\Console\Helper\HelperSet $helperSet
* @param array $commands
*
* @return \Symfony\Component\Console\Application
*/
public static function createApplication(HelperSet $helperSet, $commands = [])
{
$cli = new Application('Doctrine Migrations', MigrationsVersion::VERSION());
$cli->setCatchExceptions(true);
$cli->setHelperSet($helperSet);
self::addCommands($cli);
$cli->addCommands($commands);
return $cli;
}
/**
* @param Application $cli
*
* @return void
*/
public static function addCommands(Application $cli)
{
$cli->addCommands([
new Command\ExecuteCommand(),
new Command\GenerateCommand(),
new Command\LatestCommand(),
new Command\MigrateCommand(),
new Command\StatusCommand(),
new Command\VersionCommand(),
new Command\UpToDateCommand(),
]);
if ($cli->getHelperSet()->has('em')) {
$cli->add(new Command\DiffCommand());
}
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console\Helper;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Migrations\Configuration\ArrayConfiguration;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Doctrine\DBAL\Migrations\Configuration\JsonConfiguration;
use Doctrine\DBAL\Migrations\Configuration\XmlConfiguration;
use Doctrine\DBAL\Migrations\Configuration\YamlConfiguration;
use Doctrine\DBAL\Migrations\OutputWriter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Helper\Helper;
/**
* Class ConfigurationHelper
* @package Doctrine\DBAL\Migrations\Tools\Console\Helper
* @internal
*/
class ConfigurationHelper extends Helper implements ConfigurationHelperInterface
{
/**
* @var Connection
*/
private $connection;
/**
* @var Configuration
*/
private $configuration;
public function __construct(Connection $connection = null, Configuration $configuration = null)
{
$this->connection = $connection;
$this->configuration = $configuration;
}
public function getMigrationConfig(InputInterface $input, OutputWriter $outputWriter)
{
/**
* If a configuration option is passed to the command line, use that configuration
* instead of any other one.
*/
if ($input->getOption('configuration')) {
$outputWriter->write("Loading configuration from command option: " . $input->getOption('configuration'));
return $this->loadConfig($input->getOption('configuration'), $outputWriter);
}
/**
* If a configuration has already been set using DI or a Setter use it.
*/
if ($this->configuration) {
$outputWriter->write("Loading configuration from the integration code of your framework (setter).");
$this->configuration->setOutputWriter($outputWriter);
return $this->configuration;
}
/**
* If no any other config has been found, look for default config file in the path.
*/
$defaultConfig = [
'migrations.xml',
'migrations.yml',
'migrations.yaml',
'migrations.json',
'migrations.php',
];
foreach ($defaultConfig as $config) {
if ($this->configExists($config)) {
$outputWriter->write("Loading configuration from file: $config");
return $this->loadConfig($config, $outputWriter);
}
}
return new Configuration($this->connection, $outputWriter);
}
private function configExists($config)
{
return file_exists($config);
}
private function loadConfig($config, OutputWriter $outputWriter)
{
$map = [
'xml' => XmlConfiguration::class,
'yaml' => YamlConfiguration::class,
'yml' => YamlConfiguration::class,
'php' => ArrayConfiguration::class,
'json' => JsonConfiguration::class,
];
$info = pathinfo($config);
// check we can support this file type
if (empty($map[$info['extension']])) {
throw new \InvalidArgumentException('Given config file type is not supported');
}
$class = $map[$info['extension']];
$configuration = new $class($this->connection, $outputWriter);
$configuration->load($config);
return $configuration;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'configuration';
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types = 1);
namespace Doctrine\DBAL\Migrations\Tools\Console\Helper;
use Doctrine\DBAL\Migrations\OutputWriter;
use Symfony\Component\Console\Input\InputInterface;
interface ConfigurationHelperInterface
{
public function getMigrationConfig(InputInterface $input, OutputWriter $outputWriter);
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console\Helper;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Symfony\Component\Console\Helper\Helper;
/**
* Class ConfigurationHelper
* @package Doctrine\DBAL\Migrations\Tools\Console\Helper
* @internal
*/
class MigrationDirectoryHelper extends Helper
{
/**
* @var Configuration
*/
private $configuration;
public function __construct(Configuration $configuration = null)
{
$this->configuration = $configuration;
}
public function getMigrationDirectory()
{
$dir = $this->configuration->getMigrationsDirectory();
$dir = $dir ? $dir : getcwd();
$dir = rtrim($dir, '/');
if ( ! file_exists($dir)) {
throw new \InvalidArgumentException(sprintf('Migrations directory "%s" does not exist.', $dir));
}
if ($this->configuration->areMigrationsOrganizedByYear()) {
$dir .= $this->appendDir(date('Y'));
}
if ($this->configuration->areMigrationsOrganizedByYearAndMonth()) {
$dir .= $this->appendDir(date('m'));
}
$this->createDirIfNotExists($dir);
return $dir;
}
private function appendDir($dir)
{
return DIRECTORY_SEPARATOR . $dir;
}
private function createDirIfNotExists($dir)
{
if ( ! file_exists($dir)) {
mkdir($dir, 0755, true);
}
}
/**
* Returns the canonical name of this helper.
*
* @return string The canonical name
*
* @api
*/
public function getName()
{
return 'MigrationDirectory';
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Doctrine\DBAL\Migrations\Tools\Console\Helper;
use Doctrine\DBAL\Migrations\Configuration\AbstractFileConfiguration;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Doctrine\DBAL\Migrations\Version;
class MigrationStatusInfosHelper
{
/** @var Version[] */
private $executedMigrations;
/** @var Version[] */
private $availableMigrations;
/** @var Version[] */
private $executedUnavailableMigrations;
/** @var Configuration */
private $configuration;
public function __construct(Configuration $configuration)
{
$this->configuration = $configuration;
$this->executedMigrations = $this->configuration->getMigratedVersions();
$this->availableMigrations = $this->configuration->getAvailableVersions();
$this->executedUnavailableMigrations = array_diff($this->executedMigrations, $this->availableMigrations);
}
public function getMigrationsInfos()
{
$numExecutedUnavailableMigrations = count($this->executedUnavailableMigrations);
$numNewMigrations = count(array_diff($this->availableMigrations, $this->executedMigrations));
$infos = [
'Name' => $this->configuration->getName() ? $this->configuration->getName() : 'Doctrine Database Migrations',
'Database Driver' => $this->configuration->getConnection()->getDriver()->getName(),
'Database Name' => $this->configuration->getConnection()->getDatabase(),
'Configuration Source' => $this->configuration instanceof AbstractFileConfiguration ? $this->configuration->getFile() : 'manually configured',
'Version Table Name' => $this->configuration->getMigrationsTableName(),
'Version Column Name' => $this->configuration->getMigrationsColumnName(),
'Migrations Namespace' => $this->configuration->getMigrationsNamespace(),
'Migrations Directory' => $this->configuration->getMigrationsDirectory(),
'Previous Version' => $this->getFormattedVersionAlias('prev'),
'Current Version' => $this->getFormattedVersionAlias('current'),
'Next Version' => $this->getFormattedVersionAlias('next'),
'Latest Version' => $this->getFormattedVersionAlias('latest'),
'Executed Migrations' => count($this->executedMigrations),
'Executed Unavailable Migrations' => $numExecutedUnavailableMigrations,
'Available Migrations' => count($this->availableMigrations),
'New Migrations' => $numNewMigrations,
];
return $infos;
}
private function getFormattedVersionAlias($alias)
{
$version = $this->configuration->resolveVersionAlias($alias);
//No version found
if ($version === null) {
if ($alias === 'next') {
return 'Already at latest version';
}
if ($alias === 'prev') {
return 'Already at first version';
}
}
//Before first version "virtual" version number
if ($version === '0') {
return '<comment>0</comment>';
}
//Show normal version number
return $this->configuration->getDateTime($version) . ' (<comment>' . $version . '</comment>)';
}
/**
* @return Version[]
*/
public function getExecutedUnavailableMigrations()
{
return $this->executedUnavailableMigrations;
}
}

View File

@@ -0,0 +1,504 @@
<?php
namespace Doctrine\DBAL\Migrations;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Doctrine\DBAL\Migrations\Event\MigrationsVersionEventArgs;
use Doctrine\DBAL\Migrations\Provider\LazySchemaDiffProvider;
use Doctrine\DBAL\Migrations\Provider\SchemaDiffProvider;
use Doctrine\DBAL\Migrations\Provider\SchemaDiffProviderInterface;
use Doctrine\DBAL\Types\Type;
/**
* Class which wraps a migration version and allows execution of the
* individual migration version up or down method.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan H. Wage <jonwage@gmail.com>
*/
class Version
{
const STATE_NONE = 0;
const STATE_PRE = 1;
const STATE_EXEC = 2;
const STATE_POST = 3;
const DIRECTION_UP = 'up';
const DIRECTION_DOWN = 'down';
/**
* The Migrations Configuration instance for this migration
*
* @var Configuration
*/
private $configuration;
/**
* The OutputWriter object instance used for outputting information
*
* @var OutputWriter
*/
private $outputWriter;
/**
* The version in timestamp format (YYYYMMDDHHMMSS)
*
* @var string
*/
private $version;
/**
* The migration instance for this version
*
* @var AbstractMigration
*/
private $migration;
/**
* @var \Doctrine\DBAL\Connection
*/
private $connection;
/**
* @var string
*/
private $class;
/** The array of collected SQL statements for this version */
private $sql = [];
/** The array of collected parameters for SQL statements for this version */
private $params = [];
/** The array of collected types for SQL statements for this version */
private $types = [];
/** The time in seconds that this migration version took to execute */
private $time;
/**
* @var int
*/
private $state = self::STATE_NONE;
/** @var SchemaDiffProviderInterface */
private $schemaProvider;
public function __construct(Configuration $configuration, $version, $class, SchemaDiffProviderInterface $schemaProvider = null)
{
$this->configuration = $configuration;
$this->outputWriter = $configuration->getOutputWriter();
$this->class = $class;
$this->connection = $configuration->getConnection();
$this->migration = new $class($this);
$this->version = $version;
if ($schemaProvider !== null) {
$this->schemaProvider = $schemaProvider;
}
if ($schemaProvider === null) {
$schemaProvider = new SchemaDiffProvider(
$this->connection->getSchemaManager(),
$this->connection->getDatabasePlatform()
);
$this->schemaProvider = LazySchemaDiffProvider::fromDefaultProxyFactoryConfiguration($schemaProvider);
}
}
/**
* Returns the string version in the format YYYYMMDDHHMMSS
*
* @return string $version
*/
public function getVersion()
{
return $this->version;
}
/**
* Returns the Migrations Configuration object instance
*
* @return Configuration $configuration
*/
public function getConfiguration()
{
return $this->configuration;
}
/**
* Check if this version has been migrated or not.
*
* @return boolean
*/
public function isMigrated()
{
return $this->configuration->hasVersionMigrated($this);
}
public function markMigrated()
{
$this->markVersion('up');
}
private function markVersion($direction)
{
$action = $direction === 'up' ? 'insert' : 'delete';
$this->configuration->createMigrationTable();
$this->connection->$action(
$this->configuration->getMigrationsTableName(),
[$this->configuration->getQuotedMigrationsColumnName() => $this->version]
);
}
public function markNotMigrated()
{
$this->markVersion('down');
}
/**
* Add some SQL queries to this versions migration
*
* @param array|string $sql
* @param array $params
* @param array $types
*/
public function addSql($sql, array $params = [], array $types = [])
{
if (is_array($sql)) {
foreach ($sql as $key => $query) {
$this->sql[] = $query;
if ( ! empty($params[$key])) {
$queryTypes = $types[$key] ?? [];
$this->addQueryParams($params[$key], $queryTypes);
}
}
} else {
$this->sql[] = $sql;
if ( ! empty($params)) {
$this->addQueryParams($params, $types);
}
}
}
/**
* @param mixed[] $params Array of prepared statement parameters
* @param string[] $types Array of the types of each statement parameters
*/
private function addQueryParams($params, $types)
{
$index = count($this->sql) - 1;
$this->params[$index] = $params;
$this->types[$index] = $types;
}
/**
* Write a migration SQL file to the given path
*
* @param string $path The path to write the migration SQL file.
* @param string $direction The direction to execute.
*
* @return boolean $written
* @throws MigrationException
*/
public function writeSqlFile($path, $direction = self::DIRECTION_UP)
{
$queries = $this->execute($direction, true);
if ( ! empty($this->params)) {
throw MigrationException::migrationNotConvertibleToSql($this->class);
}
$this->outputWriter->write("\n-- Version " . $this->version . "\n");
$sqlQueries = [$this->version => $queries];
/*
* Since the configuration object changes during the creation we cannot inject things
* properly, so I had to violate LoD here (so please, let's find a way to solve it on v2).
*/
return $this->configuration->getQueryWriter()
->write($path, $direction, $sqlQueries);
}
/**
* @return AbstractMigration
*/
public function getMigration()
{
return $this->migration;
}
/**
* Execute this migration version up or down and and return the SQL.
* We are only allowing the addSql call and the schema modification to take effect in the up and down call.
* This is necessary to ensure that the migration is revertable.
* The schema is passed to the pre and post method only to be able to test the presence of some table, And the
* connection that can get used trough it allow for the test of the presence of records.
*
* @param string $direction The direction to execute the migration.
* @param boolean $dryRun Whether to not actually execute the migration SQL and just do a dry run.
* @param boolean $timeAllQueries Measuring or not the execution time of each SQL query.
*
* @return array $sql
*
* @throws \Exception when migration fails
*/
public function execute($direction, $dryRun = false, $timeAllQueries = false)
{
$this->dispatchEvent(Events::onMigrationsVersionExecuting, $direction, $dryRun);
$this->sql = [];
$transaction = $this->migration->isTransactional();
if ($transaction) {
//only start transaction if in transactional mode
$this->connection->beginTransaction();
}
try {
$migrationStart = microtime(true);
$this->state = self::STATE_PRE;
$fromSchema = $this->schemaProvider->createFromSchema();
$this->migration->{'pre' . ucfirst($direction)}($fromSchema);
if ($direction === self::DIRECTION_UP) {
$this->outputWriter->write("\n" . sprintf(' <info>++</info> migrating <comment>%s</comment>', $this->version) . "\n");
} else {
$this->outputWriter->write("\n" . sprintf(' <info>--</info> reverting <comment>%s</comment>', $this->version) . "\n");
}
$this->state = self::STATE_EXEC;
$toSchema = $this->schemaProvider->createToSchema($fromSchema);
$this->migration->$direction($toSchema);
$this->addSql($this->schemaProvider->getSqlDiffToMigrate($fromSchema, $toSchema));
$this->executeRegisteredSql($dryRun, $timeAllQueries);
$this->state = self::STATE_POST;
$this->migration->{'post' . ucfirst($direction)}($toSchema);
if ( ! $dryRun) {
if ($direction === self::DIRECTION_UP) {
$this->markMigrated();
} else {
$this->markNotMigrated();
}
}
$migrationEnd = microtime(true);
$this->time = round($migrationEnd - $migrationStart, 2);
if ($direction === self::DIRECTION_UP) {
$this->outputWriter->write(sprintf("\n <info>++</info> migrated (%ss)", $this->time));
} else {
$this->outputWriter->write(sprintf("\n <info>--</info> reverted (%ss)", $this->time));
}
if ($transaction) {
//commit only if running in transactional mode
$this->connection->commit();
}
$this->state = self::STATE_NONE;
$this->dispatchEvent(Events::onMigrationsVersionExecuted, $direction, $dryRun);
return $this->sql;
} catch (SkipMigrationException $e) {
if ($transaction) {
//only rollback transaction if in transactional mode
$this->connection->rollBack();
}
if ($dryRun === false) {
// now mark it as migrated
if ($direction === self::DIRECTION_UP) {
$this->markMigrated();
} else {
$this->markNotMigrated();
}
}
$this->outputWriter->write(sprintf("\n <info>SS</info> skipped (Reason: %s)", $e->getMessage()));
$this->state = self::STATE_NONE;
$this->dispatchEvent(Events::onMigrationsVersionSkipped, $direction, $dryRun);
return [];
} catch (\Exception $e) {
$this->outputWriter->write(sprintf(
'<error>Migration %s failed during %s. Error %s</error>',
$this->version,
$this->getExecutionState(),
$e->getMessage()
));
if ($transaction) {
//only rollback transaction if in transactional mode
$this->connection->rollBack();
}
$this->state = self::STATE_NONE;
throw $e;
}
}
public function getExecutionState()
{
switch ($this->state) {
case self::STATE_PRE:
return 'Pre-Checks';
case self::STATE_POST:
return 'Post-Checks';
case self::STATE_EXEC:
return 'Execution';
default:
return 'No State';
}
}
private function outputQueryTime($queryStart, $timeAllQueries = false)
{
if ($timeAllQueries !== false) {
$queryEnd = microtime(true);
$queryTime = round($queryEnd - $queryStart, 4);
$this->outputWriter->write(sprintf(" <info>%ss</info>", $queryTime));
}
}
/**
* Returns the time this migration version took to execute
*
* @return integer $time The time this migration version took to execute
*/
public function getTime()
{
return $this->time;
}
public function __toString()
{
return $this->version;
}
private function executeRegisteredSql($dryRun = false, $timeAllQueries = false)
{
if ( ! $dryRun) {
if ( ! empty($this->sql)) {
foreach ($this->sql as $key => $query) {
$queryStart = microtime(true);
$this->outputSqlQuery($key, $query);
if ( ! isset($this->params[$key])) {
$this->connection->executeQuery($query);
} else {
$this->connection->executeQuery($query, $this->params[$key], $this->types[$key]);
}
$this->outputQueryTime($queryStart, $timeAllQueries);
}
} else {
$this->outputWriter->write(sprintf(
'<error>Migration %s was executed but did not result in any SQL statements.</error>',
$this->version
));
}
} else {
foreach ($this->sql as $idx => $query) {
$this->outputSqlQuery($idx, $query);
}
}
}
/**
* Outputs a SQL query via the `OutputWriter`.
*
* @param int $idx The SQL query index. Used to look up params.
* @param string $query the query to output
* @return void
*/
private function outputSqlQuery($idx, $query)
{
$params = $this->formatParamsForOutput(
$this->params[$idx] ?? [],
$this->types[$idx] ?? []
);
$this->outputWriter->write(rtrim(sprintf(
' <comment>-></comment> %s %s',
$query,
$params
)));
}
/**
* Formats a set of sql parameters for output with dry run.
*
* @param array $params The query parameters
* @param array $types The types of the query params. Default type is a string
* @return string|null a string of the parameters present.
*/
private function formatParamsForOutput(array $params, array $types)
{
if (empty($params)) {
return '';
}
$out = [];
foreach ($params as $key => $value) {
$type = $types[$key] ?? 'string';
$outval = '[' . $this->formatParameter($value, $type) . ']';
$out[] = is_string($key) ? sprintf(':%s => %s', $key, $outval) : $outval;
}
return sprintf('with parameters (%s)', implode(', ', $out));
}
private function dispatchEvent($eventName, $direction, $dryRun)
{
$this->configuration->dispatchEvent($eventName, new MigrationsVersionEventArgs(
$this,
$this->configuration,
$direction,
$dryRun
));
}
private function formatParameter($value, string $type) : ?string
{
if (Type::hasType($type)) {
return Type::getType($type)->convertToDatabaseValue(
$value,
$this->connection->getDatabasePlatform()
);
}
return $this->parameterToString($value);
}
private function parameterToString($value) : string
{
if (is_array($value)) {
return implode(', ', array_map([$this, 'parameterToString'], $value));
}
if (is_int($value) || is_string($value)) {
return (string) $value;
}
if (is_bool($value)) {
return $value === true ? 'true' : 'false';
}
return '?';
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Doctrine\Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration as BaseAbstractMigration;
abstract class AbstractMigration extends BaseAbstractMigration
{
}