welcome back to dyb-tech

This commit is contained in:
Daniel Guzman
2024-05-18 02:28:01 +02:00
parent 9513cdba09
commit 9f30bc98c7
6149 changed files with 668407 additions and 0 deletions
@@ -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)];
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
}
@@ -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);
}
}
}
}
@@ -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));
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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));
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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,
));
}
}
}
@@ -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,
};
}
}
@@ -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);
}
}
@@ -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,
));
}
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Tools\Console\Exception;
use Doctrine\Migrations\Exception\MigrationException;
interface ConsoleException extends MigrationException
{
}
@@ -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.');
}
}
@@ -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));
}
}
@@ -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');
}
}
@@ -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);
}
}
@@ -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,
));
}
}
@@ -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));
}
}
@@ -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));
}
}
@@ -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;
}
@@ -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);
}
}
@@ -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,
);
}
}
@@ -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;
}
@@ -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;
}
}