welcome back to dyb-tech
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools;
|
||||
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* The BooleanStringFormatter class is responsible for formatting a string boolean representation to a PHP boolean value.
|
||||
* It is used in the XmlConfiguration class to convert the string XML boolean value to a PHP boolean value.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @see Doctrine\Migrations\Configuration\XmlConfiguration
|
||||
*/
|
||||
class BooleanStringFormatter
|
||||
{
|
||||
public static function toBoolean(string $value, bool $default): bool
|
||||
{
|
||||
return match (strtolower($value)) {
|
||||
'true', '1' => true,
|
||||
'false', '0' => false,
|
||||
default => $default,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools;
|
||||
|
||||
use function floor;
|
||||
use function log;
|
||||
use function pow;
|
||||
use function round;
|
||||
|
||||
/**
|
||||
* The BytesFormatter class is responsible for converting a bytes integer to a more human readable string.
|
||||
* This class is used to format the memory used for display purposes when executing migrations.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class BytesFormatter
|
||||
{
|
||||
public static function formatBytes(float $size, int $precision = 2): string
|
||||
{
|
||||
$base = log($size, 1024);
|
||||
$suffixes = ['', 'K', 'M', 'G', 'T'];
|
||||
|
||||
return round(pow(1024, $base - floor($base)), $precision) . $suffixes[(int) floor($base)];
|
||||
}
|
||||
}
|
||||
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Doctrine\Migrations\Exception\MigrationClassNotFound;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* The CurrentCommand class is responsible for outputting what your current version is.
|
||||
*/
|
||||
#[AsCommand(name: 'migrations:current', description: 'Outputs the current version')]
|
||||
final class CurrentCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:current';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setAliases(['current'])
|
||||
->setDescription('Outputs the current version');
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$aliasResolver = $this->getDependencyFactory()->getVersionAliasResolver();
|
||||
|
||||
$version = $aliasResolver->resolveVersionAlias('current');
|
||||
if ((string) $version === '0') {
|
||||
$description = '(No migration executed yet)';
|
||||
} else {
|
||||
try {
|
||||
$availableMigration = $this->getDependencyFactory()->getMigrationRepository()->getMigration($version);
|
||||
$description = $availableMigration->getMigration()->getDescription();
|
||||
} catch (MigrationClassNotFound) {
|
||||
$description = '(Migration info not available)';
|
||||
}
|
||||
}
|
||||
|
||||
$this->io->text(sprintf(
|
||||
"<info>%s</info>%s\n",
|
||||
(string) $version,
|
||||
$description !== '' ? ' - ' . $description : '',
|
||||
));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Doctrine\Migrations\Generator\Exception\NoChangesDetected;
|
||||
use Doctrine\Migrations\Metadata\AvailableMigrationsList;
|
||||
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
|
||||
use Doctrine\Migrations\Tools\Console\Exception\InvalidOptionUsage;
|
||||
use Doctrine\SqlFormatter\SqlFormatter;
|
||||
use OutOfBoundsException;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function addslashes;
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function count;
|
||||
use function filter_var;
|
||||
use function is_string;
|
||||
use function key;
|
||||
use function sprintf;
|
||||
|
||||
use const FILTER_VALIDATE_BOOLEAN;
|
||||
|
||||
/**
|
||||
* The DiffCommand class is responsible for generating a migration by comparing your current database schema to
|
||||
* your mapping information.
|
||||
*/
|
||||
#[AsCommand(name: 'migrations:diff', description: 'Generate a migration by comparing your current database to your mapping information.')]
|
||||
final class DiffCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:diff';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setAliases(['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>
|
||||
|
||||
EOT)
|
||||
->addOption(
|
||||
'namespace',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The namespace to use for the migration (must be in the list of configured namespaces)',
|
||||
)
|
||||
->addOption(
|
||||
'filter-expression',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Tables which are filtered by Regular Expression.',
|
||||
)
|
||||
->addOption(
|
||||
'formatted',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Format the generated SQL.',
|
||||
)
|
||||
->addOption(
|
||||
'line-length',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Max line length of unformatted lines.',
|
||||
'120',
|
||||
)
|
||||
->addOption(
|
||||
'check-database-platform',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Check Database Platform to the generated code.',
|
||||
false,
|
||||
)
|
||||
->addOption(
|
||||
'allow-empty-diff',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Do not throw an exception when no changes are detected.',
|
||||
)
|
||||
->addOption(
|
||||
'from-empty-schema',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Generate a full migration as if the current database was empty.',
|
||||
);
|
||||
}
|
||||
|
||||
/** @throws InvalidOptionUsage */
|
||||
protected function execute(
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
): int {
|
||||
$filterExpression = (string) $input->getOption('filter-expression');
|
||||
if ($filterExpression === '') {
|
||||
$filterExpression = null;
|
||||
}
|
||||
|
||||
$formatted = filter_var($input->getOption('formatted'), FILTER_VALIDATE_BOOLEAN);
|
||||
$lineLength = (int) $input->getOption('line-length');
|
||||
$allowEmptyDiff = $input->getOption('allow-empty-diff');
|
||||
$checkDbPlatform = filter_var($input->getOption('check-database-platform'), FILTER_VALIDATE_BOOLEAN);
|
||||
$fromEmptySchema = $input->getOption('from-empty-schema');
|
||||
$namespace = $input->getOption('namespace');
|
||||
if ($namespace === '') {
|
||||
$namespace = null;
|
||||
}
|
||||
|
||||
if ($formatted) {
|
||||
if (! class_exists(SqlFormatter::class)) {
|
||||
throw InvalidOptionUsage::new(
|
||||
'The "--formatted" option can only be used if the sql formatter is installed. Please run "composer require doctrine/sql-formatter".',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$configuration = $this->getDependencyFactory()->getConfiguration();
|
||||
|
||||
$dirs = $configuration->getMigrationDirectories();
|
||||
if ($namespace === null) {
|
||||
$namespace = key($dirs);
|
||||
} elseif (! isset($dirs[$namespace])) {
|
||||
throw new OutOfBoundsException(sprintf('Path not defined for the namespace %s', $namespace));
|
||||
}
|
||||
|
||||
assert(is_string($namespace));
|
||||
|
||||
$statusCalculator = $this->getDependencyFactory()->getMigrationStatusCalculator();
|
||||
$executedUnavailableMigrations = $statusCalculator->getExecutedUnavailableMigrations();
|
||||
$newMigrations = $statusCalculator->getNewMigrations();
|
||||
|
||||
if (! $this->checkNewMigrationsOrExecutedUnavailable($newMigrations, $executedUnavailableMigrations, $input, $output)) {
|
||||
$this->io->error('Migration cancelled!');
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
$fqcn = $this->getDependencyFactory()->getClassNameGenerator()->generateClassName($namespace);
|
||||
|
||||
$diffGenerator = $this->getDependencyFactory()->getDiffGenerator();
|
||||
|
||||
try {
|
||||
$path = $diffGenerator->generate(
|
||||
$fqcn,
|
||||
$filterExpression,
|
||||
$formatted,
|
||||
$lineLength,
|
||||
$checkDbPlatform,
|
||||
$fromEmptySchema,
|
||||
);
|
||||
} catch (NoChangesDetected $exception) {
|
||||
if ($allowEmptyDiff) {
|
||||
$this->io->error($exception->getMessage());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$this->io->text([
|
||||
sprintf('Generated new migration class to "<info>%s</info>"', $path),
|
||||
'',
|
||||
sprintf(
|
||||
'To run just this migration for testing purposes, you can use <info>migrations:execute --up \'%s\'</info>',
|
||||
addslashes($fqcn),
|
||||
),
|
||||
'',
|
||||
sprintf(
|
||||
'To revert the migration you can use <info>migrations:execute --down \'%s\'</info>',
|
||||
addslashes($fqcn),
|
||||
),
|
||||
'',
|
||||
]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function checkNewMigrationsOrExecutedUnavailable(
|
||||
AvailableMigrationsList $newMigrations,
|
||||
ExecutedMigrationsList $executedUnavailableMigrations,
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
): bool {
|
||||
if (count($newMigrations) === 0 && count($executedUnavailableMigrations) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (count($newMigrations) !== 0) {
|
||||
$this->io->warning(sprintf(
|
||||
'You have %d available migrations to execute.',
|
||||
count($newMigrations),
|
||||
));
|
||||
}
|
||||
|
||||
if (count($executedUnavailableMigrations) !== 0) {
|
||||
$this->io->warning(sprintf(
|
||||
'You have %d previously executed migrations in the database that are not registered migrations.',
|
||||
count($executedUnavailableMigrations),
|
||||
));
|
||||
}
|
||||
|
||||
return $this->canExecute('Are you sure you wish to continue?', $input);
|
||||
}
|
||||
}
|
||||
Vendored
+137
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Doctrine\Migrations\Configuration\Connection\ConfigurationFile;
|
||||
use Doctrine\Migrations\Configuration\Migration\ConfigurationFileWithFallback;
|
||||
use Doctrine\Migrations\DependencyFactory;
|
||||
use Doctrine\Migrations\Tools\Console\ConsoleLogger;
|
||||
use Doctrine\Migrations\Tools\Console\Exception\DependenciesNotSatisfied;
|
||||
use Doctrine\Migrations\Tools\Console\Exception\InvalidOptionUsage;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\StyleInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* The DoctrineCommand class provides base functionality for the other migrations commands to extend from.
|
||||
*/
|
||||
abstract class DoctrineCommand extends Command
|
||||
{
|
||||
/** @var StyleInterface */
|
||||
protected $io;
|
||||
|
||||
public function __construct(
|
||||
private DependencyFactory|null $dependencyFactory = null,
|
||||
string|null $name = null,
|
||||
) {
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption(
|
||||
'configuration',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The path to a migrations configuration file. <comment>[default: any of migrations.{php,xml,json,yml,yaml}]</comment>',
|
||||
);
|
||||
|
||||
$this->addOption(
|
||||
'em',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The name of the entity manager to use.',
|
||||
);
|
||||
|
||||
$this->addOption(
|
||||
'conn',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The name of the connection to use.',
|
||||
);
|
||||
|
||||
if ($this->dependencyFactory !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addOption(
|
||||
'db-configuration',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The path to a database connection configuration file.',
|
||||
'migrations-db.php',
|
||||
);
|
||||
}
|
||||
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
|
||||
$configurationParameter = $input->getOption('configuration');
|
||||
if ($this->dependencyFactory === null) {
|
||||
$configurationLoader = new ConfigurationFileWithFallback(
|
||||
is_string($configurationParameter)
|
||||
? $configurationParameter
|
||||
: null,
|
||||
);
|
||||
$connectionLoader = new ConfigurationFile($input->getOption('db-configuration'));
|
||||
$this->dependencyFactory = DependencyFactory::fromConnection($configurationLoader, $connectionLoader);
|
||||
} elseif (is_string($configurationParameter)) {
|
||||
$configurationLoader = new ConfigurationFileWithFallback($configurationParameter);
|
||||
$this->dependencyFactory->setConfigurationLoader($configurationLoader);
|
||||
}
|
||||
|
||||
$this->setNamedEmOrConnection($input);
|
||||
|
||||
if ($this->dependencyFactory->isFrozen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$logger = new ConsoleLogger($output);
|
||||
$this->dependencyFactory->setService(LoggerInterface::class, $logger);
|
||||
$this->dependencyFactory->freeze();
|
||||
}
|
||||
|
||||
protected function getDependencyFactory(): DependencyFactory
|
||||
{
|
||||
if ($this->dependencyFactory === null) {
|
||||
throw DependenciesNotSatisfied::new();
|
||||
}
|
||||
|
||||
return $this->dependencyFactory;
|
||||
}
|
||||
|
||||
protected function canExecute(string $question, InputInterface $input): bool
|
||||
{
|
||||
return ! $input->isInteractive() || $this->io->confirm($question);
|
||||
}
|
||||
|
||||
private function setNamedEmOrConnection(InputInterface $input): void
|
||||
{
|
||||
$emName = $input->getOption('em');
|
||||
$connName = $input->getOption('conn');
|
||||
if ($emName !== null && $connName !== null) {
|
||||
throw new InvalidOptionUsage('You can specify only one of the --em and --conn options.');
|
||||
}
|
||||
|
||||
if ($this->dependencyFactory->hasEntityManager() && $emName !== null) {
|
||||
$this->dependencyFactory->getConfiguration()->setEntityManagerName($emName);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($connName !== null) {
|
||||
$this->dependencyFactory->getConfiguration()->setConnectionName($connName);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+144
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Doctrine\Migrations\Tools\Console\Exception\InvalidOptionUsage;
|
||||
use Doctrine\Migrations\Tools\Console\Exception\SchemaDumpRequiresNoMigrations;
|
||||
use Doctrine\SqlFormatter\SqlFormatter;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function addslashes;
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function is_string;
|
||||
use function key;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
|
||||
/**
|
||||
* The DumpSchemaCommand class is responsible for dumping your current database schema to a migration class. This is
|
||||
* intended to be used in conjunction with the RollupCommand.
|
||||
*
|
||||
* @see Doctrine\Migrations\Tools\Console\Command\RollupCommand
|
||||
*/
|
||||
#[AsCommand(name: 'migrations:dump-schema', description: 'Dump the schema for your database to a migration.')]
|
||||
final class DumpSchemaCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:dump-schema';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setAliases(['dump-schema'])
|
||||
->setDescription('Dump the schema for your database to a migration.')
|
||||
->setHelp(<<<'EOT'
|
||||
The <info>%command.name%</info> command dumps the schema for your database to a migration:
|
||||
|
||||
<info>%command.full_name%</info>
|
||||
|
||||
After dumping your schema to a migration, you can rollup your migrations using the <info>migrations:rollup</info> command.
|
||||
EOT)
|
||||
->addOption(
|
||||
'formatted',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Format the generated SQL.',
|
||||
)
|
||||
->addOption(
|
||||
'namespace',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Namespace to use for the generated migrations (defaults to the first namespace definition).',
|
||||
)
|
||||
->addOption(
|
||||
'filter-tables',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Filter the tables to dump via Regex.',
|
||||
)
|
||||
->addOption(
|
||||
'line-length',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Max line length of unformatted lines.',
|
||||
'120',
|
||||
);
|
||||
}
|
||||
|
||||
/** @throws SchemaDumpRequiresNoMigrations */
|
||||
public function execute(
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
): int {
|
||||
$formatted = $input->getOption('formatted');
|
||||
$lineLength = (int) $input->getOption('line-length');
|
||||
|
||||
$schemaDumper = $this->getDependencyFactory()->getSchemaDumper();
|
||||
|
||||
if ($formatted) {
|
||||
if (! class_exists(SqlFormatter::class)) {
|
||||
throw InvalidOptionUsage::new(
|
||||
'The "--formatted" option can only be used if the sql formatter is installed. Please run "composer require doctrine/sql-formatter".',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$configuration = $this->getDependencyFactory()->getConfiguration();
|
||||
|
||||
$namespace = $input->getOption('namespace');
|
||||
if ($namespace === null) {
|
||||
$dirs = $configuration->getMigrationDirectories();
|
||||
$namespace = key($dirs);
|
||||
}
|
||||
|
||||
assert(is_string($namespace));
|
||||
|
||||
$this->checkNoPreviousDumpExistsForNamespace($namespace);
|
||||
|
||||
$fqcn = $this->getDependencyFactory()->getClassNameGenerator()->generateClassName($namespace);
|
||||
|
||||
$path = $schemaDumper->dump(
|
||||
$fqcn,
|
||||
$input->getOption('filter-tables'),
|
||||
$formatted,
|
||||
$lineLength,
|
||||
);
|
||||
|
||||
$this->io->text([
|
||||
sprintf('Dumped your schema to a new migration class at "<info>%s</info>"', $path),
|
||||
'',
|
||||
sprintf(
|
||||
'To run just this migration for testing purposes, you can use <info>migrations:execute --up \'%s\'</info>',
|
||||
addslashes($fqcn),
|
||||
),
|
||||
'',
|
||||
sprintf(
|
||||
'To revert the migration you can use <info>migrations:execute --down \'%s\'</info>',
|
||||
addslashes($fqcn),
|
||||
),
|
||||
'',
|
||||
'To use this as a rollup migration you can use the <info>migrations:rollup</info> command.',
|
||||
'',
|
||||
]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function checkNoPreviousDumpExistsForNamespace(string $namespace): void
|
||||
{
|
||||
$migrations = $this->getDependencyFactory()->getMigrationRepository()->getMigrations();
|
||||
foreach ($migrations->getItems() as $migration) {
|
||||
if (str_contains((string) $migration->getVersion(), $namespace)) {
|
||||
throw SchemaDumpRequiresNoMigrations::new($namespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+178
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Doctrine\Migrations\Version\Direction;
|
||||
use Doctrine\Migrations\Version\Version;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function array_map;
|
||||
use function dirname;
|
||||
use function getcwd;
|
||||
use function implode;
|
||||
use function is_dir;
|
||||
use function is_string;
|
||||
use function is_writable;
|
||||
use function sprintf;
|
||||
use function strtoupper;
|
||||
|
||||
/**
|
||||
* The ExecuteCommand class is responsible for executing migration versions up or down manually.
|
||||
*/
|
||||
#[AsCommand(name: 'migrations:execute', description: 'Execute one or more migration versions up or down manually.')]
|
||||
final class ExecuteCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:execute';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setAliases(['execute'])
|
||||
->setDescription(
|
||||
'Execute one or more migration versions up or down manually.',
|
||||
)
|
||||
->addArgument(
|
||||
'versions',
|
||||
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
|
||||
'The versions to execute.',
|
||||
null,
|
||||
)
|
||||
->addOption(
|
||||
'write-sql',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'The path to output the migration SQL file. Defaults to current working directory.',
|
||||
false,
|
||||
)
|
||||
->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 migration versions up or down manually:
|
||||
|
||||
<info>%command.full_name% FQCN</info>
|
||||
|
||||
You can show more information about the process by increasing the verbosity level. To see the
|
||||
executed queries, set the level to debug with <comment>-vv</comment>:
|
||||
|
||||
<info>%command.full_name% FQCN -vv</info>
|
||||
|
||||
If no <comment>--up</comment> or <comment>--down</comment> option is specified it defaults to up:
|
||||
|
||||
<info>%command.full_name% FQCN --down</info>
|
||||
|
||||
You can also execute the migration as a <comment>--dry-run</comment>:
|
||||
|
||||
<info>%command.full_name% FQCN --dry-run</info>
|
||||
|
||||
You can output the prepared SQL statements to a file with <comment>--write-sql</comment>:
|
||||
|
||||
<info>%command.full_name% FQCN --write-sql</info>
|
||||
|
||||
Or you can also execute the migration without a warning message which you need to interact with:
|
||||
|
||||
<info>%command.full_name% FQCN --no-interaction</info>
|
||||
|
||||
All the previous commands accept multiple migration versions, allowing you run execute more than
|
||||
one migration at once:
|
||||
|
||||
<info>%command.full_name% FQCN-1 FQCN-2 ...FQCN-n </info>
|
||||
|
||||
EOT);
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$migratorConfigurationFactory = $this->getDependencyFactory()->getConsoleInputMigratorConfigurationFactory();
|
||||
$migratorConfiguration = $migratorConfigurationFactory->getMigratorConfiguration($input);
|
||||
|
||||
$databaseName = (string) $this->getDependencyFactory()->getConnection()->getDatabase();
|
||||
$question = sprintf(
|
||||
'WARNING! You are about to execute a migration in database "%s" that could result in schema changes and data loss. Are you sure you wish to continue?',
|
||||
$databaseName === '' ? '<unnamed>' : $databaseName,
|
||||
);
|
||||
if (! $migratorConfiguration->isDryRun() && ! $this->canExecute($question, $input)) {
|
||||
$this->io->error('Migration cancelled!');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->getDependencyFactory()->getMetadataStorage()->ensureInitialized();
|
||||
|
||||
$versions = $input->getArgument('versions');
|
||||
$direction = $input->getOption('down') !== false
|
||||
? Direction::DOWN
|
||||
: Direction::UP;
|
||||
|
||||
$path = $input->getOption('write-sql') ?? getcwd();
|
||||
|
||||
if (is_string($path) && ! $this->isPathWritable($path)) {
|
||||
$this->io->error(sprintf('The path "%s" not writeable!', $path));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$planCalculator = $this->getDependencyFactory()->getMigrationPlanCalculator();
|
||||
$plan = $planCalculator->getPlanForVersions(array_map(static fn (string $version): Version => new Version($version), $versions), $direction);
|
||||
|
||||
$this->getDependencyFactory()->getLogger()->notice(
|
||||
'Executing' . ($migratorConfiguration->isDryRun() ? ' (dry-run)' : '') . ' {versions} {direction}',
|
||||
[
|
||||
'direction' => $plan->getDirection(),
|
||||
'versions' => implode(', ', $versions),
|
||||
],
|
||||
);
|
||||
|
||||
$migrator = $this->getDependencyFactory()->getMigrator();
|
||||
$sql = $migrator->migrate($plan, $migratorConfiguration);
|
||||
|
||||
if (is_string($path)) {
|
||||
$writer = $this->getDependencyFactory()->getQueryWriter();
|
||||
$writer->write($path, $direction, $sql);
|
||||
}
|
||||
|
||||
$this->io->success(sprintf(
|
||||
'Successfully migrated version(s): %s: [%s]',
|
||||
implode(', ', $versions),
|
||||
strtoupper($plan->getDirection()),
|
||||
));
|
||||
$this->io->newLine();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function isPathWritable(string $path): bool
|
||||
{
|
||||
return is_writable($path) || is_dir($path) || is_writable(dirname($path));
|
||||
}
|
||||
}
|
||||
Vendored
+89
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Exception;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function assert;
|
||||
use function is_string;
|
||||
use function key;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* The GenerateCommand class is responsible for generating a blank migration class for you to modify to your needs.
|
||||
*/
|
||||
#[AsCommand(name: 'migrations:generate', description: 'Generate a blank migration class.')]
|
||||
final class GenerateCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:generate';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setAliases(['generate'])
|
||||
->setDescription('Generate a blank migration class.')
|
||||
->addOption(
|
||||
'namespace',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The namespace to use for the migration (must be in the list of configured namespaces)',
|
||||
)
|
||||
->setHelp(<<<'EOT'
|
||||
The <info>%command.name%</info> command generates a blank migration class:
|
||||
|
||||
<info>%command.full_name%</info>
|
||||
|
||||
EOT);
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$configuration = $this->getDependencyFactory()->getConfiguration();
|
||||
|
||||
$migrationGenerator = $this->getDependencyFactory()->getMigrationGenerator();
|
||||
|
||||
$namespace = $input->getOption('namespace');
|
||||
if ($namespace === '') {
|
||||
$namespace = null;
|
||||
}
|
||||
|
||||
$dirs = $configuration->getMigrationDirectories();
|
||||
if ($namespace === null) {
|
||||
$namespace = key($dirs);
|
||||
} elseif (! isset($dirs[$namespace])) {
|
||||
throw new Exception(sprintf('Path not defined for the namespace %s', $namespace));
|
||||
}
|
||||
|
||||
assert(is_string($namespace));
|
||||
|
||||
$fqcn = $this->getDependencyFactory()->getClassNameGenerator()->generateClassName($namespace);
|
||||
|
||||
$path = $migrationGenerator->generateMigration($fqcn);
|
||||
|
||||
$this->io->text([
|
||||
sprintf('Generated new migration class to "<info>%s</info>"', $path),
|
||||
'',
|
||||
sprintf(
|
||||
'To run just this migration for testing purposes, you can use <info>migrations:execute --up \'%s\'</info>',
|
||||
$fqcn,
|
||||
),
|
||||
'',
|
||||
sprintf(
|
||||
'To revert the migration you can use <info>migrations:execute --down \'%s\'</info>',
|
||||
$fqcn,
|
||||
),
|
||||
'',
|
||||
]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Doctrine\Migrations\Exception\NoMigrationsToExecute;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* The LatestCommand class is responsible for outputting what your latest version is.
|
||||
*/
|
||||
#[AsCommand(name: 'migrations:latest', description: 'Outputs the latest version')]
|
||||
final class LatestCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:latest';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setAliases(['latest'])
|
||||
->setDescription('Outputs the latest version');
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$aliasResolver = $this->getDependencyFactory()->getVersionAliasResolver();
|
||||
|
||||
try {
|
||||
$version = $aliasResolver->resolveVersionAlias('latest');
|
||||
$availableMigration = $this->getDependencyFactory()->getMigrationRepository()->getMigration($version);
|
||||
$description = $availableMigration->getMigration()->getDescription();
|
||||
} catch (NoMigrationsToExecute) {
|
||||
$version = '0';
|
||||
$description = '';
|
||||
}
|
||||
|
||||
$this->io->text(sprintf(
|
||||
"<info>%s</info>%s\n",
|
||||
$version,
|
||||
$description !== '' ? ' - ' . $description : '',
|
||||
));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Doctrine\Migrations\Metadata\AvailableMigration;
|
||||
use Doctrine\Migrations\Metadata\AvailableMigrationsList;
|
||||
use Doctrine\Migrations\Metadata\ExecutedMigration;
|
||||
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
|
||||
use Doctrine\Migrations\Version\Version;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_unique;
|
||||
use function uasort;
|
||||
|
||||
/**
|
||||
* The ListCommand class is responsible for outputting a list of all available migrations and their status.
|
||||
*/
|
||||
#[AsCommand(name: 'migrations:list', description: 'Display a list of all available migrations and their status.')]
|
||||
final class ListCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:list';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setAliases(['list-migrations'])
|
||||
->setDescription('Display a list of all available migrations and their status.')
|
||||
->setHelp(<<<'EOT'
|
||||
The <info>%command.name%</info> command outputs a list of all available migrations and their status:
|
||||
|
||||
<info>%command.full_name%</info>
|
||||
EOT);
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$versions = $this->getSortedVersions(
|
||||
$this->getDependencyFactory()->getMigrationPlanCalculator()->getMigrations(), // available migrations
|
||||
$this->getDependencyFactory()->getMetadataStorage()->getExecutedMigrations(), // executed migrations
|
||||
);
|
||||
|
||||
$this->getDependencyFactory()->getMigrationStatusInfosHelper()->listVersions($versions, $output);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @return Version[] */
|
||||
private function getSortedVersions(AvailableMigrationsList $availableMigrations, ExecutedMigrationsList $executedMigrations): array
|
||||
{
|
||||
$availableVersions = array_map(static fn (AvailableMigration $availableMigration): Version => $availableMigration->getVersion(), $availableMigrations->getItems());
|
||||
|
||||
$executedVersions = array_map(static fn (ExecutedMigration $executedMigration): Version => $executedMigration->getVersion(), $executedMigrations->getItems());
|
||||
|
||||
$versions = array_unique(array_merge($availableVersions, $executedVersions));
|
||||
|
||||
$comparator = $this->getDependencyFactory()->getVersionComparator();
|
||||
uasort($versions, $comparator->compare(...));
|
||||
|
||||
return $versions;
|
||||
}
|
||||
}
|
||||
Vendored
+296
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Doctrine\Migrations\Exception\NoMigrationsFoundWithCriteria;
|
||||
use Doctrine\Migrations\Exception\NoMigrationsToExecute;
|
||||
use Doctrine\Migrations\Exception\UnknownMigrationVersion;
|
||||
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
|
||||
use Doctrine\Migrations\Tools\Console\ConsoleInputMigratorConfigurationFactory;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function getcwd;
|
||||
use function in_array;
|
||||
use function is_dir;
|
||||
use function is_string;
|
||||
use function is_writable;
|
||||
use function sprintf;
|
||||
use function str_starts_with;
|
||||
|
||||
/**
|
||||
* The MigrateCommand class is responsible for executing a migration from the current version to another
|
||||
* version up or down. It will calculate all the migration versions that need to be executed and execute them.
|
||||
*/
|
||||
#[AsCommand(name: 'migrations:migrate', description: 'Execute a migration to a specified version or the latest available version.')]
|
||||
final class MigrateCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:migrate';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setAliases(['migrate'])
|
||||
->setDescription(
|
||||
'Execute a migration to a specified version or the latest available version.',
|
||||
)
|
||||
->addArgument(
|
||||
'version',
|
||||
InputArgument::OPTIONAL,
|
||||
'The version FQCN 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. Defaults to current working directory.',
|
||||
false,
|
||||
)
|
||||
->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,
|
||||
'Do not throw an exception if no migration is available.',
|
||||
)
|
||||
->addOption(
|
||||
'all-or-nothing',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Wrap the entire migration in a transaction.',
|
||||
ConsoleInputMigratorConfigurationFactory::ABSENT_CONFIG_VALUE,
|
||||
)
|
||||
->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 show more information about the process by increasing the verbosity level. To see the
|
||||
executed queries, set the level to debug with <comment>-vv</comment>:
|
||||
|
||||
<info>%command.full_name% -vv</info>
|
||||
|
||||
You can optionally manually specify the version you wish to migrate to:
|
||||
|
||||
<info>%command.full_name% FQCN</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% FQCN --dry-run</info>
|
||||
|
||||
You can output the prepared SQL statements to a file with <comment>--write-sql</comment>:
|
||||
|
||||
<info>%command.full_name% FQCN --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>
|
||||
|
||||
Use the --all-or-nothing option to wrap the entire migration in a transaction.
|
||||
|
||||
EOT);
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$migratorConfigurationFactory = $this->getDependencyFactory()->getConsoleInputMigratorConfigurationFactory();
|
||||
$migratorConfiguration = $migratorConfigurationFactory->getMigratorConfiguration($input);
|
||||
|
||||
$databaseName = (string) $this->getDependencyFactory()->getConnection()->getDatabase();
|
||||
$question = sprintf(
|
||||
'WARNING! You are about to execute a migration in database "%s" that could result in schema changes and data loss. Are you sure you wish to continue?',
|
||||
$databaseName === '' ? '<unnamed>' : $databaseName,
|
||||
);
|
||||
if (! $migratorConfiguration->isDryRun() && ! $this->canExecute($question, $input)) {
|
||||
$this->io->error('Migration cancelled!');
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
$this->getDependencyFactory()->getMetadataStorage()->ensureInitialized();
|
||||
|
||||
$allowNoMigration = $input->getOption('allow-no-migration');
|
||||
$versionAlias = $input->getArgument('version');
|
||||
|
||||
$path = $input->getOption('write-sql') ?? getcwd();
|
||||
|
||||
if (is_string($path) && ! $this->isPathWritable($path)) {
|
||||
$this->io->error(sprintf('The path "%s" not writeable!', $path));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$migrationRepository = $this->getDependencyFactory()->getMigrationRepository();
|
||||
if (count($migrationRepository->getMigrations()) === 0) {
|
||||
$message = sprintf(
|
||||
'The version "%s" couldn\'t be reached, there are no registered migrations.',
|
||||
$versionAlias,
|
||||
);
|
||||
|
||||
if ($allowNoMigration) {
|
||||
$this->io->warning($message);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->io->error($message);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$version = $this->getDependencyFactory()->getVersionAliasResolver()->resolveVersionAlias($versionAlias);
|
||||
} catch (UnknownMigrationVersion) {
|
||||
$this->io->error(sprintf(
|
||||
'Unknown version: %s',
|
||||
OutputFormatter::escape($versionAlias),
|
||||
));
|
||||
|
||||
return 1;
|
||||
} catch (NoMigrationsToExecute | NoMigrationsFoundWithCriteria) {
|
||||
return $this->exitForAlias($versionAlias);
|
||||
}
|
||||
|
||||
$planCalculator = $this->getDependencyFactory()->getMigrationPlanCalculator();
|
||||
$statusCalculator = $this->getDependencyFactory()->getMigrationStatusCalculator();
|
||||
$executedUnavailableMigrations = $statusCalculator->getExecutedUnavailableMigrations();
|
||||
|
||||
if ($this->checkExecutedUnavailableMigrations($executedUnavailableMigrations, $input) === false) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
$plan = $planCalculator->getPlanUntilVersion($version);
|
||||
|
||||
if (count($plan) === 0) {
|
||||
return $this->exitForAlias($versionAlias);
|
||||
}
|
||||
|
||||
$this->getDependencyFactory()->getLogger()->notice(
|
||||
'Migrating' . ($migratorConfiguration->isDryRun() ? ' (dry-run)' : '') . ' {direction} to {to}',
|
||||
[
|
||||
'direction' => $plan->getDirection(),
|
||||
'to' => (string) $version,
|
||||
],
|
||||
);
|
||||
|
||||
$migrator = $this->getDependencyFactory()->getMigrator();
|
||||
$sql = $migrator->migrate($plan, $migratorConfiguration);
|
||||
|
||||
if (is_string($path)) {
|
||||
$writer = $this->getDependencyFactory()->getQueryWriter();
|
||||
$writer->write($path, $plan->getDirection(), $sql);
|
||||
}
|
||||
|
||||
$this->io->success(sprintf(
|
||||
'Successfully migrated to version: %s',
|
||||
$version,
|
||||
));
|
||||
$this->io->newLine();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function checkExecutedUnavailableMigrations(
|
||||
ExecutedMigrationsList $executedUnavailableMigrations,
|
||||
InputInterface $input,
|
||||
): bool {
|
||||
if (count($executedUnavailableMigrations) !== 0) {
|
||||
$this->io->warning(sprintf(
|
||||
'You have %s previously executed migrations in the database that are not registered migrations.',
|
||||
count($executedUnavailableMigrations),
|
||||
));
|
||||
|
||||
foreach ($executedUnavailableMigrations->getItems() as $executedUnavailableMigration) {
|
||||
$this->io->text(sprintf(
|
||||
'<comment>>></comment> %s (<comment>%s</comment>)',
|
||||
$executedUnavailableMigration->getExecutedAt()?->format('Y-m-d H:i:s'),
|
||||
$executedUnavailableMigration->getVersion(),
|
||||
));
|
||||
}
|
||||
|
||||
$question = 'Are you sure you wish to continue?';
|
||||
|
||||
if (! $this->canExecute($question, $input)) {
|
||||
$this->io->error('Migration cancelled!');
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function exitForAlias(string $versionAlias): int
|
||||
{
|
||||
$version = $this->getDependencyFactory()->getVersionAliasResolver()->resolveVersionAlias('current');
|
||||
|
||||
// Allow meaningful message when latest version already reached.
|
||||
if (in_array($versionAlias, ['current', 'latest', 'first'], true)) {
|
||||
$message = sprintf(
|
||||
'Already at the %s version ("%s")',
|
||||
$versionAlias,
|
||||
(string) $version,
|
||||
);
|
||||
|
||||
$this->io->success($message);
|
||||
} elseif (in_array($versionAlias, ['next', 'prev'], true) || str_starts_with($versionAlias, 'current')) {
|
||||
$message = sprintf(
|
||||
'The version "%s" couldn\'t be reached, you are at version "%s"',
|
||||
$versionAlias,
|
||||
(string) $version,
|
||||
);
|
||||
|
||||
$this->io->error($message);
|
||||
} else {
|
||||
$message = sprintf(
|
||||
'You are already at version "%s"',
|
||||
(string) $version,
|
||||
);
|
||||
|
||||
$this->io->success($message);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function isPathWritable(string $path): bool
|
||||
{
|
||||
return is_writable($path) || is_dir($path) || is_writable(dirname($path));
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* The RollupCommand class is responsible for deleting all previously executed migrations from the versions table
|
||||
* and marking the freshly dumped schema migration (that was created with DumpSchemaCommand) as migrated.
|
||||
*/
|
||||
#[AsCommand(name: 'migrations:rollup', description: 'Rollup migrations by deleting all tracked versions and insert the one version that exists.')]
|
||||
final class RollupCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:rollup';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setAliases(['rollup'])
|
||||
->setDescription('Rollup migrations by deleting all tracked versions and insert the one version that exists.')
|
||||
->setHelp(<<<'EOT'
|
||||
The <info>%command.name%</info> command rolls up migrations by deleting all tracked versions and
|
||||
inserts the one version that exists that was created with the <info>migrations:dump-schema</info> command.
|
||||
|
||||
<info>%command.full_name%</info>
|
||||
|
||||
To dump your schema to a migration version you can use the <info>migrations:dump-schema</info> command.
|
||||
EOT);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$question = sprintf(
|
||||
'WARNING! You are about to execute a migration in database "%s" that could result in schema changes and data loss. Are you sure you wish to continue?',
|
||||
$this->getDependencyFactory()->getConnection()->getDatabase() ?? '<unnamed>',
|
||||
);
|
||||
|
||||
if (! $this->canExecute($question, $input)) {
|
||||
$this->io->error('Migration cancelled!');
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
$this->getDependencyFactory()->getMetadataStorage()->ensureInitialized();
|
||||
$version = $this->getDependencyFactory()->getRollup()->rollup();
|
||||
|
||||
$this->io->success(sprintf(
|
||||
'Rolled up migrations to version %s',
|
||||
(string) $version,
|
||||
));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* The StatusCommand class is responsible for outputting what the current state is of all your migrations. It shows
|
||||
* what your current version is, how many new versions you have to execute, etc. and details about each of your migrations.
|
||||
*/
|
||||
#[AsCommand(name: 'migrations:status', description: 'View the status of a set of migrations.')]
|
||||
final class StatusCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:status';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setAliases(['status'])
|
||||
->setDescription('View the status of a set of migrations.')
|
||||
->setHelp(<<<'EOT'
|
||||
The <info>%command.name%</info> command outputs the status of a set of migrations:
|
||||
|
||||
<info>%command.full_name%</info>
|
||||
EOT);
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$infosHelper = $this->getDependencyFactory()->getMigrationStatusInfosHelper();
|
||||
$infosHelper->showMigrationsInfo($output);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(name: 'migrations:sync-metadata-storage', description: 'Ensures that the metadata storage is at the latest version.')]
|
||||
final class SyncMetadataCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:sync-metadata-storage';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setAliases(['sync-metadata-storage'])
|
||||
->setDescription('Ensures that the metadata storage is at the latest version.')
|
||||
->setHelp(<<<'EOT'
|
||||
The way metadata is stored in the database can change between releases.
|
||||
The <info>%command.name%</info> command updates metadata storage to the latest version,
|
||||
ensuring it is ready to receive migrations generated by the current version of Doctrine Migrations.
|
||||
|
||||
|
||||
<info>%command.full_name%</info>
|
||||
EOT);
|
||||
}
|
||||
|
||||
public function execute(
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
): int {
|
||||
$this->getDependencyFactory()->getMetadataStorage()->ensureInitialized();
|
||||
|
||||
$this->io->success('Metadata storage synchronized');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Vendored
+111
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Doctrine\Migrations\Metadata\AvailableMigration;
|
||||
use Doctrine\Migrations\Metadata\AvailableMigrationsList;
|
||||
use Doctrine\Migrations\Metadata\ExecutedMigration;
|
||||
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
|
||||
use Doctrine\Migrations\Version\Version;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_unique;
|
||||
use function count;
|
||||
use function sprintf;
|
||||
use function uasort;
|
||||
|
||||
/**
|
||||
* The UpToDateCommand class outputs if your database is up to date or if there are new migrations
|
||||
* that need to be executed.
|
||||
*/
|
||||
#[AsCommand(name: 'migrations:up-to-date', description: 'Tells you if your schema is up-to-date.')]
|
||||
final class UpToDateCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:up-to-date';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setAliases(['up-to-date'])
|
||||
->setDescription('Tells you if your schema is up-to-date.')
|
||||
->addOption('fail-on-unregistered', 'u', InputOption::VALUE_NONE, 'Whether to fail when there are unregistered extra migrations found')
|
||||
->addOption('list-migrations', 'l', InputOption::VALUE_NONE, 'Show a list of missing or not migrated versions.')
|
||||
->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();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$statusCalculator = $this->getDependencyFactory()->getMigrationStatusCalculator();
|
||||
|
||||
$executedUnavailableMigrations = $statusCalculator->getExecutedUnavailableMigrations();
|
||||
$newMigrations = $statusCalculator->getNewMigrations();
|
||||
$newMigrationsCount = count($newMigrations);
|
||||
$executedUnavailableMigrationsCount = count($executedUnavailableMigrations);
|
||||
|
||||
if ($newMigrationsCount === 0 && $executedUnavailableMigrationsCount === 0) {
|
||||
$this->io->success('Up-to-date! No migrations to execute.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$exitCode = 0;
|
||||
if ($newMigrationsCount > 0) {
|
||||
$this->io->error(sprintf(
|
||||
'Out-of-date! %u migration%s available to execute.',
|
||||
$newMigrationsCount,
|
||||
$newMigrationsCount > 1 ? 's are' : ' is',
|
||||
));
|
||||
$exitCode = 1;
|
||||
}
|
||||
|
||||
if ($executedUnavailableMigrationsCount > 0) {
|
||||
$this->io->error(sprintf(
|
||||
'You have %1$u previously executed migration%3$s in the database that %2$s registered migration%3$s.',
|
||||
$executedUnavailableMigrationsCount,
|
||||
$executedUnavailableMigrationsCount > 1 ? 'are not' : 'is not a',
|
||||
$executedUnavailableMigrationsCount > 1 ? 's' : '',
|
||||
));
|
||||
if ($input->getOption('fail-on-unregistered')) {
|
||||
$exitCode = 2;
|
||||
}
|
||||
}
|
||||
|
||||
if ($input->getOption('list-migrations')) {
|
||||
$versions = $this->getSortedVersions($newMigrations, $executedUnavailableMigrations);
|
||||
$this->getDependencyFactory()->getMigrationStatusInfosHelper()->listVersions($versions, $output);
|
||||
|
||||
$this->io->newLine();
|
||||
}
|
||||
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
/** @return Version[] */
|
||||
private function getSortedVersions(AvailableMigrationsList $newMigrations, ExecutedMigrationsList $executedUnavailableMigrations): array
|
||||
{
|
||||
$executedUnavailableVersion = array_map(static fn (ExecutedMigration $executedMigration): Version => $executedMigration->getVersion(), $executedUnavailableMigrations->getItems());
|
||||
|
||||
$newVersions = array_map(static fn (AvailableMigration $availableMigration): Version => $availableMigration->getVersion(), $newMigrations->getItems());
|
||||
|
||||
$versions = array_unique(array_merge($executedUnavailableVersion, $newVersions));
|
||||
|
||||
$comparator = $this->getDependencyFactory()->getVersionComparator();
|
||||
uasort($versions, $comparator->compare(...));
|
||||
|
||||
return $versions;
|
||||
}
|
||||
}
|
||||
Vendored
+260
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Command;
|
||||
|
||||
use Doctrine\Migrations\Exception\MigrationClassNotFound;
|
||||
use Doctrine\Migrations\Exception\UnknownMigrationVersion;
|
||||
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
|
||||
use Doctrine\Migrations\Tools\Console\Exception\InvalidOptionUsage;
|
||||
use Doctrine\Migrations\Tools\Console\Exception\VersionAlreadyExists;
|
||||
use Doctrine\Migrations\Tools\Console\Exception\VersionDoesNotExist;
|
||||
use Doctrine\Migrations\Version\Direction;
|
||||
use Doctrine\Migrations\Version\ExecutionResult;
|
||||
use Doctrine\Migrations\Version\Version;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* The VersionCommand class is responsible for manually adding and deleting migration versions from the tracking table.
|
||||
*/
|
||||
#[AsCommand(name: 'migrations:version', description: 'Manually add and delete migration versions from the version table.')]
|
||||
final class VersionCommand extends DoctrineCommand
|
||||
{
|
||||
/** @var string|null */
|
||||
protected static $defaultName = 'migrations:version';
|
||||
|
||||
private bool $markMigrated;
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setAliases(['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% MIGRATION-FQCN --add</info>
|
||||
|
||||
If you want to delete a version you can use the <comment>--delete</comment> option:
|
||||
|
||||
<info>%command.full_name% MIGRATION-FQCN --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=MIGRATION-FQCN --range-to=MIGRATION-FQCN</info>
|
||||
<info>%command.full_name% --delete --range-from=MIGRATION-FQCN --range-to=MIGRATION-FQCN</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();
|
||||
}
|
||||
|
||||
/** @throws InvalidOptionUsage */
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
if ($input->getOption('add') === false && $input->getOption('delete') === false) {
|
||||
throw InvalidOptionUsage::new('You must specify whether you want to --add or --delete the specified version.');
|
||||
}
|
||||
|
||||
$this->markMigrated = $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?';
|
||||
|
||||
$confirmation = $this->io->confirm($question);
|
||||
|
||||
if ($confirmation) {
|
||||
$this->markVersions($input, $output);
|
||||
} else {
|
||||
$this->io->error('Migration cancelled!');
|
||||
}
|
||||
} else {
|
||||
$this->markVersions($input, $output);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @throws InvalidOptionUsage */
|
||||
private function markVersions(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$affectedVersion = $input->getArgument('version');
|
||||
$allOption = $input->getOption('all');
|
||||
$rangeFromOption = $input->getOption('range-from');
|
||||
$rangeToOption = $input->getOption('range-to');
|
||||
|
||||
if ($allOption === true && ($rangeFromOption !== null || $rangeToOption !== null)) {
|
||||
throw InvalidOptionUsage::new(
|
||||
'Options --all and --range-to/--range-from both used. You should use only one of them.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($rangeFromOption !== null xor $rangeToOption !== null) {
|
||||
throw InvalidOptionUsage::new(
|
||||
'Options --range-to and --range-from should be used together.',
|
||||
);
|
||||
}
|
||||
|
||||
$executedMigrations = $this->getDependencyFactory()->getMetadataStorage()->getExecutedMigrations();
|
||||
$availableVersions = $this->getDependencyFactory()->getMigrationPlanCalculator()->getMigrations();
|
||||
if ($allOption === true) {
|
||||
if ($input->getOption('delete') === true) {
|
||||
foreach ($executedMigrations->getItems() as $availableMigration) {
|
||||
$this->mark($input, $output, $availableMigration->getVersion(), false, $executedMigrations);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($availableVersions->getItems() as $availableMigration) {
|
||||
$this->mark($input, $output, $availableMigration->getVersion(), true, $executedMigrations);
|
||||
}
|
||||
} elseif ($affectedVersion !== null) {
|
||||
$this->mark($input, $output, new Version($affectedVersion), false, $executedMigrations);
|
||||
} elseif ($rangeFromOption !== null && $rangeToOption !== null) {
|
||||
$migrate = false;
|
||||
foreach ($availableVersions->getItems() as $availableMigration) {
|
||||
if ((string) $availableMigration->getVersion() === $rangeFromOption) {
|
||||
$migrate = true;
|
||||
}
|
||||
|
||||
if ($migrate) {
|
||||
$this->mark($input, $output, $availableMigration->getVersion(), true, $executedMigrations);
|
||||
}
|
||||
|
||||
if ((string) $availableMigration->getVersion() === $rangeToOption) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw InvalidOptionUsage::new('You must specify the version or use the --all argument.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws VersionAlreadyExists
|
||||
* @throws VersionDoesNotExist
|
||||
* @throws UnknownMigrationVersion
|
||||
*/
|
||||
private function mark(InputInterface $input, OutputInterface $output, Version $version, bool $all, ExecutedMigrationsList $executedMigrations): void
|
||||
{
|
||||
try {
|
||||
$availableMigration = $this->getDependencyFactory()->getMigrationRepository()->getMigration($version);
|
||||
} catch (MigrationClassNotFound) {
|
||||
$availableMigration = null;
|
||||
}
|
||||
|
||||
$storage = $this->getDependencyFactory()->getMetadataStorage();
|
||||
if ($availableMigration === null) {
|
||||
if ($input->getOption('delete') === false) {
|
||||
throw UnknownMigrationVersion::new((string) $version);
|
||||
}
|
||||
|
||||
$question =
|
||||
'WARNING! You are about to delete a migration version from the version table that has no corresponding migration file.' .
|
||||
'Do you want to delete this migration from the migrations table?';
|
||||
|
||||
$confirmation = $this->io->confirm($question);
|
||||
|
||||
if ($confirmation) {
|
||||
$migrationResult = new ExecutionResult($version, Direction::DOWN);
|
||||
$storage->complete($migrationResult);
|
||||
$this->io->text(sprintf(
|
||||
"<info>%s</info> deleted from the version table.\n",
|
||||
(string) $version,
|
||||
));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$marked = false;
|
||||
|
||||
if ($this->markMigrated && $executedMigrations->hasMigration($version)) {
|
||||
if (! $all) {
|
||||
throw VersionAlreadyExists::new($version);
|
||||
}
|
||||
|
||||
$marked = true;
|
||||
}
|
||||
|
||||
if (! $this->markMigrated && ! $executedMigrations->hasMigration($version)) {
|
||||
if (! $all) {
|
||||
throw VersionDoesNotExist::new($version);
|
||||
}
|
||||
|
||||
$marked = true;
|
||||
}
|
||||
|
||||
if ($marked === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->markMigrated) {
|
||||
$migrationResult = new ExecutionResult($version, Direction::UP);
|
||||
$storage->complete($migrationResult);
|
||||
|
||||
$this->io->text(sprintf(
|
||||
"<info>%s</info> added to the version table.\n",
|
||||
(string) $version,
|
||||
));
|
||||
} else {
|
||||
$migrationResult = new ExecutionResult($version, Direction::DOWN);
|
||||
$storage->complete($migrationResult);
|
||||
|
||||
$this->io->text(sprintf(
|
||||
"<info>%s</info> deleted from the version table.\n",
|
||||
(string) $version,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console;
|
||||
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\Migrations\Configuration\Configuration;
|
||||
use Doctrine\Migrations\MigratorConfiguration;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
class ConsoleInputMigratorConfigurationFactory implements MigratorConfigurationFactory
|
||||
{
|
||||
public const ABSENT_CONFIG_VALUE = 'notprovided';
|
||||
|
||||
public function __construct(private readonly Configuration $configuration)
|
||||
{
|
||||
}
|
||||
|
||||
public function getMigratorConfiguration(InputInterface $input): MigratorConfiguration
|
||||
{
|
||||
$timeAllQueries = $input->hasOption('query-time') ? (bool) $input->getOption('query-time') : false;
|
||||
$dryRun = $input->hasOption('dry-run') ? (bool) $input->getOption('dry-run') : false;
|
||||
$allOrNothing = $this->determineAllOrNothingValueFrom($input) ?? $this->configuration->isAllOrNothing();
|
||||
|
||||
return (new MigratorConfiguration())
|
||||
->setDryRun($dryRun)
|
||||
->setTimeAllQueries($timeAllQueries)
|
||||
->setAllOrNothing($allOrNothing);
|
||||
}
|
||||
|
||||
private function determineAllOrNothingValueFrom(InputInterface $input): bool|null
|
||||
{
|
||||
$allOrNothingOption = null;
|
||||
$wasOptionExplicitlyPassed = $input->hasOption('all-or-nothing');
|
||||
|
||||
if ($wasOptionExplicitlyPassed) {
|
||||
$allOrNothingOption = $input->getOption('all-or-nothing');
|
||||
}
|
||||
|
||||
if ($wasOptionExplicitlyPassed && ($allOrNothingOption !== null && $allOrNothingOption !== self::ABSENT_CONFIG_VALUE)) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/migrations',
|
||||
'https://github.com/doctrine/migrations/issues/1304',
|
||||
<<<'DEPRECATION'
|
||||
Context: Passing values to option `--all-or-nothing`
|
||||
Problem: Passing values is deprecated
|
||||
Solution: If you need to disable the behavior, omit the option,
|
||||
otherwise, pass the option without a value
|
||||
DEPRECATION,
|
||||
);
|
||||
}
|
||||
|
||||
return match ($allOrNothingOption) {
|
||||
self::ABSENT_CONFIG_VALUE => null,
|
||||
null => false,
|
||||
default => (bool) $allOrNothingOption,
|
||||
};
|
||||
}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use Psr\Log\AbstractLogger;
|
||||
use Psr\Log\InvalidArgumentException;
|
||||
use Psr\Log\LogLevel;
|
||||
use Stringable;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function gettype;
|
||||
use function is_object;
|
||||
use function is_scalar;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use function strtr;
|
||||
|
||||
/**
|
||||
* PSR-3 compliant console logger.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @see https://www.php-fig.org/psr/psr-3/
|
||||
*/
|
||||
final class ConsoleLogger extends AbstractLogger
|
||||
{
|
||||
public const INFO = 'info';
|
||||
public const ERROR = 'error';
|
||||
|
||||
/** @var array<string, int> */
|
||||
private array $verbosityLevelMap = [
|
||||
LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL,
|
||||
LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL,
|
||||
LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL,
|
||||
LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL,
|
||||
LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL,
|
||||
LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL,
|
||||
LogLevel::INFO => OutputInterface::VERBOSITY_VERBOSE,
|
||||
LogLevel::DEBUG => OutputInterface::VERBOSITY_VERY_VERBOSE,
|
||||
];
|
||||
/** @var array<string, string> */
|
||||
private array $formatLevelMap = [
|
||||
LogLevel::EMERGENCY => self::ERROR,
|
||||
LogLevel::ALERT => self::ERROR,
|
||||
LogLevel::CRITICAL => self::ERROR,
|
||||
LogLevel::ERROR => self::ERROR,
|
||||
LogLevel::WARNING => self::INFO,
|
||||
LogLevel::NOTICE => self::INFO,
|
||||
LogLevel::INFO => self::INFO,
|
||||
LogLevel::DEBUG => self::INFO,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<string, int> $verbosityLevelMap
|
||||
* @param array<string, string> $formatLevelMap
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly OutputInterface $output,
|
||||
array $verbosityLevelMap = [],
|
||||
array $formatLevelMap = [],
|
||||
) {
|
||||
$this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap;
|
||||
$this->formatLevelMap = $formatLevelMap + $this->formatLevelMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param mixed[] $context
|
||||
*/
|
||||
public function log($level, $message, array $context = []): void
|
||||
{
|
||||
if (! isset($this->verbosityLevelMap[$level])) {
|
||||
throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level));
|
||||
}
|
||||
|
||||
$output = $this->output;
|
||||
|
||||
// Write to the error output if necessary and available
|
||||
if ($this->formatLevelMap[$level] === self::ERROR) {
|
||||
if ($this->output instanceof ConsoleOutputInterface) {
|
||||
$output = $output->getErrorOutput();
|
||||
}
|
||||
}
|
||||
|
||||
// the if condition check isn't necessary -- it's the same one that $output will do internally anyway.
|
||||
// We only do it for efficiency here as the message formatting is relatively expensive.
|
||||
if ($output->getVerbosity() < $this->verbosityLevelMap[$level]) {
|
||||
return;
|
||||
}
|
||||
|
||||
$output->writeln(sprintf('<%1$s>[%2$s] %3$s</%1$s>', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates context values into the message placeholders.
|
||||
*
|
||||
* @param mixed[] $context
|
||||
*/
|
||||
private function interpolate(string|Stringable $message, array $context): string
|
||||
{
|
||||
$message = (string) $message;
|
||||
if (! str_contains($message, '{')) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
$replacements = [];
|
||||
foreach ($context as $key => $val) {
|
||||
if ($val === null || is_scalar($val) || $val instanceof Stringable) {
|
||||
$replacements["{{$key}}"] = $val;
|
||||
} elseif ($val instanceof DateTimeInterface) {
|
||||
$replacements["{{$key}}"] = $val->format(DateTime::RFC3339);
|
||||
} elseif (is_object($val)) {
|
||||
$replacements["{{$key}}"] = '[object ' . $val::class . ']';
|
||||
} else {
|
||||
$replacements["{{$key}}"] = '[' . gettype($val) . ']';
|
||||
}
|
||||
|
||||
if (! isset($replacements["{{$key}}"])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$replacements["{{$key}}"] = '<comment>' . $replacements["{{$key}}"] . '</comment>';
|
||||
}
|
||||
|
||||
return strtr($message, $replacements);
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
use Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager;
|
||||
use Doctrine\Migrations\Configuration\Migration\ConfigurationFileWithFallback;
|
||||
use Doctrine\Migrations\DependencyFactory;
|
||||
use Doctrine\Migrations\Tools\Console\Command\CurrentCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\DiffCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\DoctrineCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\DumpSchemaCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\ExecuteCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\GenerateCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\LatestCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\ListCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\MigrateCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\RollupCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\StatusCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\SyncMetadataCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\UpToDateCommand;
|
||||
use Doctrine\Migrations\Tools\Console\Command\VersionCommand;
|
||||
use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\HelperSet;
|
||||
|
||||
use function assert;
|
||||
use function file_exists;
|
||||
use function getcwd;
|
||||
use function is_readable;
|
||||
use function sprintf;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
/**
|
||||
* The ConsoleRunner class is used to create the Symfony Console application for the Doctrine Migrations console.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @see bin/doctrine-migrations.php
|
||||
*/
|
||||
class ConsoleRunner
|
||||
{
|
||||
public static function findDependencyFactory(): DependencyFactory|null
|
||||
{
|
||||
// Support for using the Doctrine ORM convention of providing a `cli-config.php` file.
|
||||
$configurationDirectories = [
|
||||
getcwd(),
|
||||
getcwd() . DIRECTORY_SEPARATOR . 'config',
|
||||
];
|
||||
|
||||
$configurationFile = null;
|
||||
foreach ($configurationDirectories as $configurationDirectory) {
|
||||
$configurationFilePath = $configurationDirectory . DIRECTORY_SEPARATOR . 'cli-config.php';
|
||||
|
||||
if (! file_exists($configurationFilePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$configurationFile = $configurationFilePath;
|
||||
break;
|
||||
}
|
||||
|
||||
$dependencyFactory = null;
|
||||
if ($configurationFile !== null) {
|
||||
if (! is_readable($configurationFile)) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'Configuration file "%s" cannot be read.',
|
||||
$configurationFile,
|
||||
));
|
||||
}
|
||||
|
||||
$dependencyFactory = require $configurationFile;
|
||||
$dependencyFactory = self::checkLegacyConfiguration($dependencyFactory, $configurationFile);
|
||||
}
|
||||
|
||||
if ($dependencyFactory !== null && ! ($dependencyFactory instanceof DependencyFactory)) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'Configuration file "%s" must return an instance of "%s"',
|
||||
$configurationFile,
|
||||
DependencyFactory::class,
|
||||
));
|
||||
}
|
||||
|
||||
return $dependencyFactory;
|
||||
}
|
||||
|
||||
/** @param DoctrineCommand[] $commands */
|
||||
public static function run(array $commands = [], DependencyFactory|null $dependencyFactory = null): void
|
||||
{
|
||||
$cli = static::createApplication($commands, $dependencyFactory);
|
||||
$cli->run();
|
||||
}
|
||||
|
||||
/** @param DoctrineCommand[] $commands */
|
||||
public static function createApplication(array $commands = [], DependencyFactory|null $dependencyFactory = null): Application
|
||||
{
|
||||
$version = InstalledVersions::getVersion('doctrine/migrations');
|
||||
assert($version !== null);
|
||||
$cli = new Application('Doctrine Migrations', $version);
|
||||
$cli->setCatchExceptions(true);
|
||||
self::addCommands($cli, $dependencyFactory);
|
||||
$cli->addCommands($commands);
|
||||
|
||||
return $cli;
|
||||
}
|
||||
|
||||
public static function addCommands(Application $cli, DependencyFactory|null $dependencyFactory = null): void
|
||||
{
|
||||
$cli->addCommands([
|
||||
new CurrentCommand($dependencyFactory),
|
||||
new DumpSchemaCommand($dependencyFactory),
|
||||
new ExecuteCommand($dependencyFactory),
|
||||
new GenerateCommand($dependencyFactory),
|
||||
new LatestCommand($dependencyFactory),
|
||||
new MigrateCommand($dependencyFactory),
|
||||
new RollupCommand($dependencyFactory),
|
||||
new StatusCommand($dependencyFactory),
|
||||
new VersionCommand($dependencyFactory),
|
||||
new UpToDateCommand($dependencyFactory),
|
||||
new SyncMetadataCommand($dependencyFactory),
|
||||
new ListCommand($dependencyFactory),
|
||||
]);
|
||||
|
||||
if ($dependencyFactory === null || ! $dependencyFactory->hasSchemaProvider()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cli->add(new DiffCommand($dependencyFactory));
|
||||
}
|
||||
|
||||
private static function checkLegacyConfiguration(mixed $dependencyFactory, string $configurationFile): mixed
|
||||
{
|
||||
if (! ($dependencyFactory instanceof HelperSet)) {
|
||||
return $dependencyFactory;
|
||||
}
|
||||
|
||||
$configurations = new ConfigurationFileWithFallback();
|
||||
if ($dependencyFactory->has('em') && $dependencyFactory->get('em') instanceof EntityManagerHelper) {
|
||||
return DependencyFactory::fromEntityManager(
|
||||
$configurations,
|
||||
new ExistingEntityManager($dependencyFactory->get('em')->getEntityManager()),
|
||||
);
|
||||
}
|
||||
|
||||
throw new RuntimeException(sprintf(
|
||||
'Configuration HelperSet returned by "%s" does not have a valid "em" or the "db" helper.',
|
||||
$configurationFile,
|
||||
));
|
||||
}
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Exception;
|
||||
|
||||
use Doctrine\Migrations\Exception\MigrationException;
|
||||
|
||||
interface ConsoleException extends MigrationException
|
||||
{
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Exception;
|
||||
|
||||
use LogicException;
|
||||
|
||||
final class DependenciesNotSatisfied extends LogicException implements ConsoleException
|
||||
{
|
||||
public static function new(): self
|
||||
{
|
||||
return new self('The dependency factory has not been initialized or provided.');
|
||||
}
|
||||
}
|
||||
vendor/doctrine/migrations/lib/Doctrine/Migrations/Tools/Console/Exception/DirectoryDoesNotExist.php
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Exception;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class DirectoryDoesNotExist extends InvalidArgumentException implements ConsoleException
|
||||
{
|
||||
public static function new(string $directory): self
|
||||
{
|
||||
return new self(sprintf('Migrations directory "%s" does not exist.', $directory));
|
||||
}
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Exception;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class FileTypeNotSupported extends InvalidArgumentException implements ConsoleException
|
||||
{
|
||||
public static function new(): self
|
||||
{
|
||||
return new self('Given config file type is not supported');
|
||||
}
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Exception;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class InvalidOptionUsage extends InvalidArgumentException implements ConsoleException
|
||||
{
|
||||
public static function new(string $explanation): self
|
||||
{
|
||||
return new self($explanation);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class SchemaDumpRequiresNoMigrations extends RuntimeException implements ConsoleException
|
||||
{
|
||||
public static function new(string $namespace): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Delete any previous migrations in the namespace "%s" before dumping your schema.',
|
||||
$namespace,
|
||||
));
|
||||
}
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Exception;
|
||||
|
||||
use Doctrine\Migrations\Version\Version;
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class VersionAlreadyExists extends InvalidArgumentException implements ConsoleException
|
||||
{
|
||||
public static function new(Version $version): self
|
||||
{
|
||||
return new self(sprintf('The version "%s" already exists in the version table.', (string) $version));
|
||||
}
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Exception;
|
||||
|
||||
use Doctrine\Migrations\Version\Version;
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class VersionDoesNotExist extends InvalidArgumentException implements ConsoleException
|
||||
{
|
||||
public static function new(Version $version): self
|
||||
{
|
||||
return new self(sprintf('The version "%s" does not exist in the version table.', (string) $version));
|
||||
}
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Helper;
|
||||
|
||||
use Doctrine\Migrations\Configuration\Configuration;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* The ConfigurationHelper defines the interface for getting the Configuration instance to be used for migrations.
|
||||
*/
|
||||
interface ConfigurationHelper
|
||||
{
|
||||
public function getConfiguration(
|
||||
InputInterface $input,
|
||||
): Configuration;
|
||||
}
|
||||
vendor/doctrine/migrations/lib/Doctrine/Migrations/Tools/Console/Helper/MigrationDirectoryHelper.php
Vendored
+59
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Helper;
|
||||
|
||||
use Doctrine\Migrations\Configuration\Configuration;
|
||||
use Doctrine\Migrations\Tools\Console\Exception\DirectoryDoesNotExist;
|
||||
|
||||
use function date;
|
||||
use function file_exists;
|
||||
use function mkdir;
|
||||
use function rtrim;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
/**
|
||||
* The MigrationDirectoryHelper class is responsible for returning the directory that migrations are stored in.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MigrationDirectoryHelper
|
||||
{
|
||||
/** @throws DirectoryDoesNotExist */
|
||||
public function getMigrationDirectory(Configuration $configuration, string $dir): string
|
||||
{
|
||||
$dir = rtrim($dir, '/');
|
||||
|
||||
if (! file_exists($dir)) {
|
||||
throw DirectoryDoesNotExist::new($dir);
|
||||
}
|
||||
|
||||
if ($configuration->areMigrationsOrganizedByYear()) {
|
||||
$dir .= $this->appendDir(date('Y'));
|
||||
}
|
||||
|
||||
if ($configuration->areMigrationsOrganizedByYearAndMonth()) {
|
||||
$dir .= $this->appendDir(date('m'));
|
||||
}
|
||||
|
||||
$this->createDirIfNotExists($dir);
|
||||
|
||||
return $dir;
|
||||
}
|
||||
|
||||
private function appendDir(string $dir): string
|
||||
{
|
||||
return DIRECTORY_SEPARATOR . $dir;
|
||||
}
|
||||
|
||||
private function createDirIfNotExists(string $dir): void
|
||||
{
|
||||
if (file_exists($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console\Helper;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\Migrations\Configuration\Configuration;
|
||||
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
|
||||
use Doctrine\Migrations\Metadata\Storage\MetadataStorage;
|
||||
use Doctrine\Migrations\Metadata\Storage\TableMetadataStorageConfiguration;
|
||||
use Doctrine\Migrations\Version\AliasResolver;
|
||||
use Doctrine\Migrations\Version\MigrationPlanCalculator;
|
||||
use Doctrine\Migrations\Version\MigrationStatusCalculator;
|
||||
use Doctrine\Migrations\Version\Version;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Helper\TableCell;
|
||||
use Symfony\Component\Console\Helper\TableSeparator;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Throwable;
|
||||
|
||||
use function array_unshift;
|
||||
use function count;
|
||||
use function get_class;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* The MigrationStatusInfosHelper class is responsible for building the array of information used when displaying
|
||||
* the status of your migrations.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @see Doctrine\Migrations\Tools\Console\Command\StatusCommand
|
||||
*/
|
||||
class MigrationStatusInfosHelper
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Configuration $configuration,
|
||||
private readonly Connection $connection,
|
||||
private readonly AliasResolver $aliasResolver,
|
||||
private readonly MigrationPlanCalculator $migrationPlanCalculator,
|
||||
private readonly MigrationStatusCalculator $statusCalculator,
|
||||
private readonly MetadataStorage $metadataStorage,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @param Version[] $versions */
|
||||
public function listVersions(array $versions, OutputInterface $output): void
|
||||
{
|
||||
$table = new Table($output);
|
||||
$table->setHeaders(
|
||||
[
|
||||
[new TableCell('Migration Versions', ['colspan' => 4])],
|
||||
['Migration', 'Status', 'Migrated At', 'Execution Time', 'Description'],
|
||||
],
|
||||
);
|
||||
$executedMigrations = $this->metadataStorage->getExecutedMigrations();
|
||||
$availableMigrations = $this->migrationPlanCalculator->getMigrations();
|
||||
|
||||
foreach ($versions as $version) {
|
||||
$description = null;
|
||||
$executedAt = null;
|
||||
$executionTime = null;
|
||||
|
||||
if ($executedMigrations->hasMigration($version)) {
|
||||
$executedMigration = $executedMigrations->getMigration($version);
|
||||
$executionTime = $executedMigration->getExecutionTime();
|
||||
$executedAt = $executedMigration->getExecutedAt() instanceof DateTimeInterface
|
||||
? $executedMigration->getExecutedAt()->format('Y-m-d H:i:s')
|
||||
: null;
|
||||
}
|
||||
|
||||
if ($availableMigrations->hasMigration($version)) {
|
||||
$description = $availableMigrations->getMigration($version)->getMigration()->getDescription();
|
||||
}
|
||||
|
||||
if ($executedMigrations->hasMigration($version) && $availableMigrations->hasMigration($version)) {
|
||||
$status = '<info>migrated</info>';
|
||||
} elseif ($executedMigrations->hasMigration($version)) {
|
||||
$status = '<error>migrated, not available</error>';
|
||||
} else {
|
||||
$status = '<comment>not migrated</comment>';
|
||||
}
|
||||
|
||||
$table->addRow([
|
||||
(string) $version,
|
||||
$status,
|
||||
(string) $executedAt,
|
||||
$executionTime !== null ? $executionTime . 's' : '',
|
||||
$description,
|
||||
]);
|
||||
}
|
||||
|
||||
$table->render();
|
||||
}
|
||||
|
||||
public function showMigrationsInfo(OutputInterface $output): void
|
||||
{
|
||||
$executedMigrations = $this->metadataStorage->getExecutedMigrations();
|
||||
$availableMigrations = $this->migrationPlanCalculator->getMigrations();
|
||||
|
||||
$newMigrations = $this->statusCalculator->getNewMigrations();
|
||||
$executedUnavailableMigrations = $this->statusCalculator->getExecutedUnavailableMigrations();
|
||||
|
||||
$storage = $this->configuration->getMetadataStorageConfiguration();
|
||||
|
||||
$table = new Table($output);
|
||||
$table->setHeaders(
|
||||
[
|
||||
[new TableCell('Configuration', ['colspan' => 3])],
|
||||
],
|
||||
);
|
||||
|
||||
$dataGroup = [
|
||||
'Storage' => [
|
||||
'Type' => $storage !== null ? $storage::class : null,
|
||||
],
|
||||
'Database' => [
|
||||
'Driver' => get_class($this->connection->getDriver()),
|
||||
'Name' => $this->connection->getDatabase(),
|
||||
],
|
||||
'Versions' => [
|
||||
'Previous' => $this->getFormattedVersionAlias('prev', $executedMigrations),
|
||||
'Current' => $this->getFormattedVersionAlias('current', $executedMigrations),
|
||||
'Next' => $this->getFormattedVersionAlias('next', $executedMigrations),
|
||||
'Latest' => $this->getFormattedVersionAlias('latest', $executedMigrations),
|
||||
],
|
||||
|
||||
'Migrations' => [
|
||||
'Executed' => count($executedMigrations),
|
||||
'Executed Unavailable' => count($executedUnavailableMigrations) > 0 ? ('<error>' . count($executedUnavailableMigrations) . '</error>') : '0',
|
||||
'Available' => count($availableMigrations),
|
||||
'New' => count($newMigrations) > 0 ? ('<question>' . count($newMigrations) . '</question>') : '0',
|
||||
],
|
||||
'Migration Namespaces' => $this->configuration->getMigrationDirectories(),
|
||||
|
||||
];
|
||||
if ($storage instanceof TableMetadataStorageConfiguration) {
|
||||
$dataGroup['Storage'] += [
|
||||
'Table Name' => $storage->getTableName(),
|
||||
'Column Name' => $storage->getVersionColumnName(),
|
||||
];
|
||||
}
|
||||
|
||||
$first = true;
|
||||
foreach ($dataGroup as $group => $dataValues) {
|
||||
$nsRows = [];
|
||||
foreach ($dataValues as $k => $v) {
|
||||
$nsRows[] = [
|
||||
$k,
|
||||
$v,
|
||||
];
|
||||
}
|
||||
|
||||
if (count($nsRows) <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $first) {
|
||||
$table->addRow([new TableSeparator(['colspan' => 3])]);
|
||||
}
|
||||
|
||||
$first = false;
|
||||
array_unshift(
|
||||
$nsRows[0],
|
||||
new TableCell('<info>' . $group . '</info>', ['rowspan' => count($dataValues)]),
|
||||
);
|
||||
$table->addRows($nsRows);
|
||||
}
|
||||
|
||||
$table->render();
|
||||
}
|
||||
|
||||
private function getFormattedVersionAlias(string $alias, ExecutedMigrationsList $executedMigrations): string
|
||||
{
|
||||
try {
|
||||
$version = $this->aliasResolver->resolveVersionAlias($alias);
|
||||
} catch (Throwable) {
|
||||
$version = null;
|
||||
}
|
||||
|
||||
// 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 ((string) $version === '0') {
|
||||
return '<comment>0</comment>';
|
||||
}
|
||||
|
||||
// Show normal version number
|
||||
return sprintf(
|
||||
'<comment>%s </comment>',
|
||||
(string) $version,
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools\Console;
|
||||
|
||||
use Doctrine\Migrations\MigratorConfiguration;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
interface MigratorConfigurationFactory
|
||||
{
|
||||
public function getMigratorConfiguration(InputInterface $input): MigratorConfiguration;
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Tools;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use LogicException;
|
||||
use PDO;
|
||||
|
||||
use function method_exists;
|
||||
|
||||
/** @internal */
|
||||
final class TransactionHelper
|
||||
{
|
||||
public static function commitIfInTransaction(Connection $connection): void
|
||||
{
|
||||
if (! self::inTransaction($connection)) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/migrations',
|
||||
'https://github.com/doctrine/migrations/issues/1169',
|
||||
<<<'DEPRECATION'
|
||||
Context: trying to commit a transaction
|
||||
Problem: the transaction is already committed, relying on silencing is deprecated.
|
||||
Solution: override `AbstractMigration::isTransactional()` so that it returns false.
|
||||
Automate that by setting `transactional` to false in the configuration.
|
||||
More details at https://www.doctrine-project.org/projects/doctrine-migrations/en/stable/explanation/implicit-commits.html
|
||||
DEPRECATION,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$connection->commit();
|
||||
}
|
||||
|
||||
public static function rollbackIfInTransaction(Connection $connection): void
|
||||
{
|
||||
if (! self::inTransaction($connection)) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/migrations',
|
||||
'https://github.com/doctrine/migrations/issues/1169',
|
||||
<<<'DEPRECATION'
|
||||
Context: trying to rollback a transaction
|
||||
Problem: the transaction is already rolled back, relying on silencing is deprecated.
|
||||
Solution: override `AbstractMigration::isTransactional()` so that it returns false.
|
||||
Automate that by setting `transactional` to false in the configuration.
|
||||
More details at https://www.doctrine-project.org/projects/doctrine-migrations/en/stable/explanation/implicit-commits.html
|
||||
DEPRECATION,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$connection->rollBack();
|
||||
}
|
||||
|
||||
private static function inTransaction(Connection $connection): bool
|
||||
{
|
||||
$innermostConnection = self::getInnerConnection($connection);
|
||||
|
||||
/* Attempt to commit or rollback while no transaction is running
|
||||
results in an exception since PHP 8 + pdo_mysql combination */
|
||||
return ! $innermostConnection instanceof PDO || $innermostConnection->inTransaction();
|
||||
}
|
||||
|
||||
/** @return object|resource|null */
|
||||
private static function getInnerConnection(Connection $connection)
|
||||
{
|
||||
try {
|
||||
return $connection->getNativeConnection();
|
||||
} catch (LogicException) {
|
||||
}
|
||||
|
||||
$innermostConnection = $connection;
|
||||
while (method_exists($innermostConnection, 'getWrappedConnection')) {
|
||||
$innermostConnection = $innermostConnection->getWrappedConnection();
|
||||
}
|
||||
|
||||
return $innermostConnection;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user