This commit is contained in:
Marek
2026-03-24 00:04:55 +01:00
commit c5229e48ed
4225 changed files with 511461 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\Exception\AbortMigration;
use Doctrine\Migrations\Exception\FrozenMigration;
use Doctrine\Migrations\Exception\IrreversibleMigration;
use Doctrine\Migrations\Exception\MigrationException;
use Doctrine\Migrations\Exception\SkipMigration;
use Doctrine\Migrations\Query\Query;
use Psr\Log\LoggerInterface;
use function sprintf;
/**
* The AbstractMigration class is for end users to extend from when creating migrations. Extend this class
* and implement the required up() and down() methods.
*/
abstract class AbstractMigration
{
/** @var Connection */
protected $connection;
/** @var AbstractSchemaManager<AbstractPlatform> */
protected $sm;
/** @var AbstractPlatform */
protected $platform;
/** @var Query[] */
private array $plannedSql = [];
private bool $frozen = false;
public function __construct(Connection $connection, private readonly LoggerInterface $logger)
{
$this->connection = $connection;
$this->sm = $this->connection->createSchemaManager();
$this->platform = $this->connection->getDatabasePlatform();
}
/**
* Indicates the transactional mode of this migration.
*
* If this function returns true (default) the migration will be executed
* in one transaction, otherwise non-transactional state will be used to
* execute each of the migration SQLs.
*
* Extending class should override this function to alter the return value.
*/
public function isTransactional(): bool
{
return true;
}
public function getDescription(): string
{
return '';
}
public function warnIf(bool $condition, string $message = 'Unknown Reason'): void
{
if (! $condition) {
return;
}
$this->logger->warning($message, ['migration' => $this]);
}
/** @throws AbortMigration */
public function abortIf(bool $condition, string $message = 'Unknown Reason'): void
{
if ($condition) {
throw new AbortMigration($message);
}
}
/** @throws SkipMigration */
public function skipIf(bool $condition, string $message = 'Unknown Reason'): void
{
if ($condition) {
throw new SkipMigration($message);
}
}
/** @throws MigrationException|DBALException */
public function preUp(Schema $schema): void
{
}
/** @throws MigrationException|DBALException */
public function postUp(Schema $schema): void
{
}
/** @throws MigrationException|DBALException */
public function preDown(Schema $schema): void
{
}
/** @throws MigrationException|DBALException */
public function postDown(Schema $schema): void
{
}
/** @throws MigrationException|DBALException */
abstract public function up(Schema $schema): void;
/** @throws MigrationException|DBALException */
public function down(Schema $schema): void
{
$this->abortIf(true, sprintf('No down() migration implemented for "%s"', static::class));
}
/**
* @param mixed[] $params
* @param mixed[] $types
*/
protected function addSql(
string $sql,
array $params = [],
array $types = [],
): void {
if ($this->frozen) {
throw FrozenMigration::new();
}
$this->plannedSql[] = new Query($sql, $params, $types);
}
/** @return Query[] */
public function getSql(): array
{
return $this->plannedSql;
}
public function freeze(): void
{
$this->frozen = true;
}
protected function write(string $message): void
{
$this->logger->notice($message, ['migration' => $this]);
}
/** @throws IrreversibleMigration */
protected function throwIrreversibleMigrationException(string|null $message = null): void
{
if ($message === null) {
$message = 'This migration is irreversible and cannot be reverted.';
}
throw new IrreversibleMigration($message);
}
}

View File

@@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration;
use Doctrine\Migrations\Configuration\Exception\FrozenConfiguration;
use Doctrine\Migrations\Configuration\Exception\UnknownConfigurationValue;
use Doctrine\Migrations\Exception\MigrationException;
use Doctrine\Migrations\Metadata\Storage\MetadataStorageConfiguration;
use function strtolower;
/**
* The Configuration class is responsible for defining migration configuration information.
*/
final class Configuration
{
public const VERSIONS_ORGANIZATION_NONE = 'none';
public const VERSIONS_ORGANIZATION_BY_YEAR = 'year';
public const VERSIONS_ORGANIZATION_BY_YEAR_AND_MONTH = 'year_and_month';
/** @var array<string, string> */
private array $migrationsDirectories = [];
/** @var string[] */
private array $migrationClasses = [];
private bool $migrationsAreOrganizedByYear = false;
private bool $migrationsAreOrganizedByYearAndMonth = false;
private string|null $customTemplate = null;
private bool $isDryRun = false;
private bool $allOrNothing = false;
private bool $transactional = true;
private string|null $connectionName = null;
private string|null $entityManagerName = null;
private bool $checkDbPlatform = true;
private MetadataStorageConfiguration|null $metadataStorageConfiguration = null;
private bool $frozen = false;
public function freeze(): void
{
$this->frozen = true;
}
private function assertNotFrozen(): void
{
if ($this->frozen) {
throw FrozenConfiguration::new();
}
}
public function setMetadataStorageConfiguration(MetadataStorageConfiguration $metadataStorageConfiguration): void
{
$this->assertNotFrozen();
$this->metadataStorageConfiguration = $metadataStorageConfiguration;
}
/** @return string[] */
public function getMigrationClasses(): array
{
return $this->migrationClasses;
}
public function addMigrationClass(string $className): void
{
$this->assertNotFrozen();
$this->migrationClasses[] = $className;
}
public function getMetadataStorageConfiguration(): MetadataStorageConfiguration|null
{
return $this->metadataStorageConfiguration;
}
public function addMigrationsDirectory(string $namespace, string $path): void
{
$this->assertNotFrozen();
$this->migrationsDirectories[$namespace] = $path;
}
/** @return array<string,string> */
public function getMigrationDirectories(): array
{
return $this->migrationsDirectories;
}
public function getConnectionName(): string|null
{
return $this->connectionName;
}
public function setConnectionName(string|null $connectionName): void
{
$this->assertNotFrozen();
$this->connectionName = $connectionName;
}
public function getEntityManagerName(): string|null
{
return $this->entityManagerName;
}
public function setEntityManagerName(string|null $entityManagerName): void
{
$this->assertNotFrozen();
$this->entityManagerName = $entityManagerName;
}
public function setCustomTemplate(string|null $customTemplate): void
{
$this->assertNotFrozen();
$this->customTemplate = $customTemplate;
}
public function getCustomTemplate(): string|null
{
return $this->customTemplate;
}
public function areMigrationsOrganizedByYear(): bool
{
return $this->migrationsAreOrganizedByYear;
}
/** @throws MigrationException */
public function setMigrationsAreOrganizedByYear(
bool $migrationsAreOrganizedByYear = true,
): void {
$this->assertNotFrozen();
$this->migrationsAreOrganizedByYear = $migrationsAreOrganizedByYear;
}
/** @throws MigrationException */
public function setMigrationsAreOrganizedByYearAndMonth(
bool $migrationsAreOrganizedByYearAndMonth = true,
): void {
$this->assertNotFrozen();
$this->migrationsAreOrganizedByYear = $migrationsAreOrganizedByYearAndMonth;
$this->migrationsAreOrganizedByYearAndMonth = $migrationsAreOrganizedByYearAndMonth;
}
public function areMigrationsOrganizedByYearAndMonth(): bool
{
return $this->migrationsAreOrganizedByYearAndMonth;
}
public function setIsDryRun(bool $isDryRun): void
{
$this->assertNotFrozen();
$this->isDryRun = $isDryRun;
}
public function isDryRun(): bool
{
return $this->isDryRun;
}
public function setAllOrNothing(bool $allOrNothing): void
{
$this->assertNotFrozen();
$this->allOrNothing = $allOrNothing;
}
public function isAllOrNothing(): bool
{
return $this->allOrNothing;
}
public function setTransactional(bool $transactional): void
{
$this->assertNotFrozen();
$this->transactional = $transactional;
}
public function isTransactional(): bool
{
return $this->transactional;
}
public function setCheckDatabasePlatform(bool $checkDbPlatform): void
{
$this->checkDbPlatform = $checkDbPlatform;
}
public function isDatabasePlatformChecked(): bool
{
return $this->checkDbPlatform;
}
public function setMigrationOrganization(string $migrationOrganization): void
{
$this->assertNotFrozen();
match (strtolower($migrationOrganization)) {
self::VERSIONS_ORGANIZATION_NONE => $this->setMigrationsAreOrganizedByYearAndMonth(false),
self::VERSIONS_ORGANIZATION_BY_YEAR => $this->setMigrationsAreOrganizedByYear(),
self::VERSIONS_ORGANIZATION_BY_YEAR_AND_MONTH => $this->setMigrationsAreOrganizedByYearAndMonth(),
default => throw UnknownConfigurationValue::new('organize_migrations', $migrationOrganization),
};
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Connection;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\Migrations\Configuration\Connection\Exception\FileNotFound;
use Doctrine\Migrations\Configuration\Connection\Exception\InvalidConfiguration;
use InvalidArgumentException;
use function file_exists;
use function is_array;
/**
* This class will return a Connection instance, loaded from a configuration file provided as argument.
*/
final class ConfigurationFile implements ConnectionLoader
{
public function __construct(private readonly string $filename)
{
}
public function getConnection(string|null $name = null): Connection
{
if ($name !== null) {
throw new InvalidArgumentException('Only one connection is supported');
}
if (! file_exists($this->filename)) {
throw FileNotFound::new($this->filename);
}
$params = include $this->filename;
if ($params instanceof Connection) {
return $params;
}
if ($params instanceof ConnectionLoader) {
return $params->getConnection();
}
if (is_array($params)) {
return DriverManager::getConnection($params);
}
throw InvalidConfiguration::invalidArrayConfiguration();
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Connection;
use Doctrine\DBAL\Connection;
use Doctrine\Migrations\Configuration\Connection\Exception\ConnectionNotSpecified;
/**
* The ConnectionLoader defines the interface used to load the Doctrine\DBAL\Connection instance to use
* for migrations.
*/
interface ConnectionLoader
{
/**
* Read the input and return a Connection, returns null if the config
* is not supported.
*
* @throws ConnectionNotSpecified
*/
public function getConnection(string|null $name = null): Connection;
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Connection;
use Doctrine\DBAL\Connection;
use Doctrine\Migrations\Configuration\Connection\Exception\InvalidConfiguration;
use Doctrine\Persistence\ConnectionRegistry;
final class ConnectionRegistryConnection implements ConnectionLoader
{
private ConnectionRegistry $registry;
private string|null $defaultConnectionName = null;
public static function withSimpleDefault(ConnectionRegistry $registry, string|null $connectionName = null): self
{
$that = new self();
$that->registry = $registry;
$that->defaultConnectionName = $connectionName;
return $that;
}
private function __construct()
{
}
public function getConnection(string|null $name = null): Connection
{
$connection = $this->registry->getConnection($name ?? $this->defaultConnectionName);
if (! $connection instanceof Connection) {
throw InvalidConfiguration::invalidConnectionType($connection);
}
return $connection;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Connection\Exception;
use InvalidArgumentException;
final class ConnectionNotSpecified extends InvalidArgumentException implements LoaderException
{
public static function new(): self
{
return new self(
'You have to specify a --db-configuration file or pass a Database Connection as a dependency to the Migrations.',
);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Connection\Exception;
use InvalidArgumentException;
use function sprintf;
final class FileNotFound extends InvalidArgumentException implements LoaderException
{
public static function new(string $file): self
{
return new self(sprintf('Database configuration file "%s" does not exist.', $file));
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Connection\Exception;
use Doctrine\DBAL\Connection;
use InvalidArgumentException;
use function get_debug_type;
use function sprintf;
final class InvalidConfiguration extends InvalidArgumentException implements LoaderException
{
public static function invalidArrayConfiguration(): self
{
return new self('The connection file has to return an array with database configuration parameters.');
}
public static function invalidConnectionType(object $connection): self
{
return new self(sprintf(
'The returned connection must be a %s instance, %s returned.',
Connection::class,
get_debug_type($connection),
));
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Connection\Exception;
use Doctrine\Migrations\Exception\MigrationException;
interface LoaderException extends MigrationException
{
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Connection;
use Doctrine\DBAL\Connection;
use Doctrine\Migrations\Configuration\Exception\InvalidLoader;
final class ExistingConnection implements ConnectionLoader
{
public function __construct(private readonly Connection $connection)
{
}
public function getConnection(string|null $name = null): Connection
{
if ($name !== null) {
throw InvalidLoader::noMultipleConnections($this);
}
return $this->connection;
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\EntityManager;
use Doctrine\Migrations\Configuration\EntityManager\Exception\FileNotFound;
use Doctrine\Migrations\Configuration\EntityManager\Exception\InvalidConfiguration;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use function file_exists;
/**
* This class will return an EntityManager instance, loaded from a configuration file provided as argument.
*/
final class ConfigurationFile implements EntityManagerLoader
{
public function __construct(private readonly string $filename)
{
}
/**
* Read the input and return a Configuration, returns null if the config
* is not supported.
*
* @throws InvalidConfiguration
*/
public function getEntityManager(string|null $name = null): EntityManagerInterface
{
if ($name !== null) {
throw new InvalidArgumentException('Only one connection is supported');
}
if (! file_exists($this->filename)) {
throw FileNotFound::new($this->filename);
}
$params = include $this->filename;
if ($params instanceof EntityManagerInterface) {
return $params;
}
if ($params instanceof EntityManagerLoader) {
return $params->getEntityManager();
}
throw InvalidConfiguration::invalidArrayConfiguration();
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
/**
* The EntityManagerLoader defines the interface used to load the Doctrine\DBAL\EntityManager instance to use
* for migrations.
*
* @internal
*/
interface EntityManagerLoader
{
public function getEntityManager(string|null $name = null): EntityManagerInterface;
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\EntityManager\Exception;
use InvalidArgumentException;
use function sprintf;
final class FileNotFound extends InvalidArgumentException implements LoaderException
{
public static function new(string $file): self
{
return new self(sprintf('Database configuration file "%s" does not exist.', $file));
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\EntityManager\Exception;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use function get_debug_type;
use function sprintf;
final class InvalidConfiguration extends InvalidArgumentException implements LoaderException
{
public static function invalidArrayConfiguration(): self
{
return new self('The EntityManager file has to return an array with database configuration parameters.');
}
public static function invalidManagerType(object $em): self
{
return new self(sprintf(
'The returned manager must implement %s, %s returned.',
EntityManagerInterface::class,
get_debug_type($em),
));
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\EntityManager\Exception;
use Doctrine\Migrations\Exception\MigrationException;
interface LoaderException extends MigrationException
{
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\EntityManager;
use Doctrine\Migrations\Configuration\Exception\InvalidLoader;
use Doctrine\ORM\EntityManagerInterface;
final class ExistingEntityManager implements EntityManagerLoader
{
public function __construct(private readonly EntityManagerInterface $entityManager)
{
}
public function getEntityManager(string|null $name = null): EntityManagerInterface
{
if ($name !== null) {
throw InvalidLoader::noMultipleEntityManagers($this);
}
return $this->entityManager;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\EntityManager;
use Doctrine\Migrations\Configuration\EntityManager\Exception\InvalidConfiguration;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry;
final class ManagerRegistryEntityManager implements EntityManagerLoader
{
private ManagerRegistry $registry;
private string|null $defaultManagerName = null;
public static function withSimpleDefault(ManagerRegistry $registry, string|null $managerName = null): self
{
$that = new self();
$that->registry = $registry;
$that->defaultManagerName = $managerName;
return $that;
}
private function __construct()
{
}
public function getEntityManager(string|null $name = null): EntityManagerInterface
{
$managerName = $name ?? $this->defaultManagerName;
$em = $this->registry->getManager($managerName);
if (! $em instanceof EntityManagerInterface) {
throw InvalidConfiguration::invalidManagerType($em);
}
return $em;
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Exception;
use Doctrine\Migrations\Exception\MigrationException;
interface ConfigurationException extends MigrationException
{
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Exception;
use InvalidArgumentException;
use function sprintf;
final class FileNotFound extends InvalidArgumentException implements ConfigurationException
{
public static function new(string $file): self
{
return new self(sprintf('The "%s" configuration file does not exist.', $file));
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Exception;
use LogicException;
final class FrozenConfiguration extends LogicException implements ConfigurationException
{
public static function new(): self
{
return new self('The configuration is frozen and cannot be edited anymore.');
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Exception;
use Doctrine\Migrations\Configuration\Connection\ConnectionLoader;
use Doctrine\Migrations\Configuration\EntityManager\EntityManagerLoader;
use InvalidArgumentException;
use function get_debug_type;
use function sprintf;
final class InvalidLoader extends InvalidArgumentException implements ConfigurationException
{
public static function noMultipleConnections(ConnectionLoader $loader): self
{
return new self(sprintf(
'Only one connection is supported by %s',
get_debug_type($loader),
));
}
public static function noMultipleEntityManagers(EntityManagerLoader $loader): self
{
return new self(sprintf(
'Only one entity manager is supported by %s',
get_debug_type($loader),
));
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Exception;
use LogicException;
use function sprintf;
use function var_export;
final class UnknownConfigurationValue extends LogicException implements ConfigurationException
{
public static function new(string $key, mixed $value): self
{
return new self(
sprintf(
'Unknown %s for configuration "%s".',
var_export($value, true),
$key,
),
10,
);
}
}

View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration;
use Closure;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Configuration\Migration\Exception\InvalidConfigurationKey;
use Doctrine\Migrations\Metadata\Storage\TableMetadataStorageConfiguration;
use Doctrine\Migrations\Tools\BooleanStringFormatter;
use function assert;
use function call_user_func;
use function is_array;
use function is_bool;
use function is_callable;
final class ConfigurationArray implements ConfigurationLoader
{
/** @param array<string,mixed> $configurations */
public function __construct(private readonly array $configurations)
{
}
public function getConfiguration(): Configuration
{
$configMap = [
'migrations_paths' => static function ($paths, Configuration $configuration): void {
foreach ($paths as $namespace => $path) {
$configuration->addMigrationsDirectory($namespace, $path);
}
},
'migrations' => static function ($migrations, Configuration $configuration): void {
foreach ($migrations as $className) {
$configuration->addMigrationClass($className);
}
},
'connection' => 'setConnectionName',
'em' => 'setEntityManagerName',
'table_storage' => [
'table_name' => 'setTableName',
'version_column_name' => 'setVersionColumnName',
'version_column_length' => static function ($value, TableMetadataStorageConfiguration $configuration): void {
$configuration->setVersionColumnLength((int) $value);
},
'executed_at_column_name' => 'setExecutedAtColumnName',
'execution_time_column_name' => 'setExecutionTimeColumnName',
],
'organize_migrations' => 'setMigrationOrganization',
'custom_template' => 'setCustomTemplate',
'all_or_nothing' => static function ($value, Configuration $configuration): void {
$configuration->setAllOrNothing(is_bool($value) ? $value : BooleanStringFormatter::toBoolean($value, false));
},
'transactional' => static function ($value, Configuration $configuration): void {
$configuration->setTransactional(is_bool($value) ? $value : BooleanStringFormatter::toBoolean($value, true));
},
'check_database_platform' => static function ($value, Configuration $configuration): void {
$configuration->setCheckDatabasePlatform(is_bool($value) ? $value : BooleanStringFormatter::toBoolean($value, false));
},
];
$object = new Configuration();
self::applyConfigs($configMap, $object, $this->configurations);
if ($object->getMetadataStorageConfiguration() === null) {
$object->setMetadataStorageConfiguration(new TableMetadataStorageConfiguration());
}
return $object;
}
/**
* @param mixed[] $configMap
* @param array<string|int,mixed> $data
*/
private static function applyConfigs(array $configMap, Configuration|TableMetadataStorageConfiguration $object, array $data): void
{
foreach ($data as $configurationKey => $configurationValue) {
if (! isset($configMap[$configurationKey])) {
throw InvalidConfigurationKey::new((string) $configurationKey);
}
if (is_array($configMap[$configurationKey])) {
if ($configurationKey !== 'table_storage') {
throw InvalidConfigurationKey::new((string) $configurationKey);
}
$storageConfig = new TableMetadataStorageConfiguration();
assert($object instanceof Configuration);
$object->setMetadataStorageConfiguration($storageConfig);
self::applyConfigs($configMap[$configurationKey], $storageConfig, $configurationValue);
} else {
$callable = $configMap[$configurationKey] instanceof Closure
? $configMap[$configurationKey]
: [$object, $configMap[$configurationKey]];
assert(is_callable($callable));
call_user_func(
$callable,
$configurationValue,
$object,
$data,
);
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration;
use function dirname;
use function realpath;
abstract class ConfigurationFile implements ConfigurationLoader
{
/** @var string */
protected $file;
public function __construct(string $file)
{
$this->file = $file;
}
/**
* @param array<string,string> $directories
*
* @return array<string,string>
*/
final protected function getDirectoriesRelativeToFile(array $directories, string $file): array
{
foreach ($directories as $ns => $dir) {
$path = realpath(dirname($file) . '/' . $dir);
$directories[$ns] = $path !== false ? $path : $dir;
}
return $directories;
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Configuration\Migration\Exception\MissingConfigurationFile;
use Doctrine\Migrations\Tools\Console\Exception\FileTypeNotSupported;
use function file_exists;
/**
* This class creates a configuration instance from a configuration file passed as argument.
* If no arguments are provided, will try to load one of migrations.{xml, yml, yaml, json, php} files.
*
* @internal
*/
final class ConfigurationFileWithFallback implements ConfigurationLoader
{
public function __construct(private readonly string|null $file = null)
{
}
public function getConfiguration(): Configuration
{
if ($this->file !== null) {
return $this->loadConfiguration($this->file);
}
/**
* If no config has been provided, look for default config file in the path.
*/
$defaultFiles = [
'migrations.xml',
'migrations.yml',
'migrations.yaml',
'migrations.json',
'migrations.php',
];
foreach ($defaultFiles as $file) {
if ($this->configurationFileExists($file)) {
return $this->loadConfiguration($file);
}
}
throw MissingConfigurationFile::new();
}
private function configurationFileExists(string $config): bool
{
return file_exists($config);
}
/** @throws FileTypeNotSupported */
private function loadConfiguration(string $file): Configuration
{
return (new FormattedFile($file))->getConfiguration();
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration;
use Doctrine\Migrations\Configuration\Configuration;
interface ConfigurationLoader
{
public function getConfiguration(): Configuration;
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration\Exception;
use Doctrine\Migrations\Configuration\Exception\ConfigurationException;
use LogicException;
use function sprintf;
final class InvalidConfigurationFormat extends LogicException implements ConfigurationException
{
public static function new(string $file): self
{
return new self(sprintf('Configuration file "%s" cannot be parsed.', $file));
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration\Exception;
use Doctrine\Migrations\Configuration\Exception\ConfigurationException;
use LogicException;
use function sprintf;
final class InvalidConfigurationKey extends LogicException implements ConfigurationException
{
public static function new(string $key): self
{
return new self(sprintf('Migrations configuration key "%s" does not exist.', $key), 10);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration\Exception;
use Doctrine\Migrations\Configuration\Exception\ConfigurationException;
use LogicException;
final class JsonNotValid extends LogicException implements ConfigurationException
{
public static function new(): self
{
return new self('Configuration is not valid JSON.', 10);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration\Exception;
use Doctrine\Migrations\Configuration\Exception\ConfigurationException;
use LogicException;
final class MissingConfigurationFile extends LogicException implements ConfigurationException
{
public static function new(): self
{
return new self('It was not possible to locate any configuration file.');
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration\Exception;
use Doctrine\Migrations\Configuration\Exception\ConfigurationException;
use LogicException;
final class XmlNotValid extends LogicException implements ConfigurationException
{
public static function malformed(): self
{
return new self('The XML configuration is malformed.');
}
public static function failedValidation(): self
{
return new self('XML configuration did not pass the validation test.', 10);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration\Exception;
use Doctrine\Migrations\Configuration\Exception\ConfigurationException;
use LogicException;
final class YamlNotAvailable extends LogicException implements ConfigurationException
{
public static function new(): self
{
return new self(
'Unable to load yaml configuration files, please run '
. '`composer require symfony/yaml` to load yaml configuration files.',
);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration\Exception;
use Doctrine\Migrations\Configuration\Exception\ConfigurationException;
use LogicException;
final class YamlNotValid extends LogicException implements ConfigurationException
{
public static function malformed(): self
{
return new self('The YAML configuration is malformed.');
}
public static function invalid(): self
{
return new self('Configuration is not valid YAML.', 10);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration;
use Doctrine\Migrations\Configuration\Configuration;
final class ExistingConfiguration implements ConfigurationLoader
{
public function __construct(private readonly Configuration $configurations)
{
}
public function getConfiguration(): Configuration
{
return $this->configurations;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Configuration\Migration\Exception\InvalidConfigurationFormat;
use function count;
use function pathinfo;
use const PATHINFO_EXTENSION;
/** @internal */
final class FormattedFile extends ConfigurationFile
{
/** @var callable[] */
private array $loaders = [];
private function setDefaultLoaders(): void
{
$this->loaders = [
'json' => static fn ($file): ConfigurationLoader => new JsonFile($file),
'php' => static fn ($file): ConfigurationLoader => new PhpFile($file),
'xml' => static fn ($file): ConfigurationLoader => new XmlFile($file),
'yaml' => static fn ($file): ConfigurationLoader => new YamlFile($file),
'yml' => static fn ($file): ConfigurationLoader => new YamlFile($file),
];
}
public function getConfiguration(): Configuration
{
if (count($this->loaders) === 0) {
$this->setDefaultLoaders();
}
$extension = pathinfo($this->file, PATHINFO_EXTENSION);
if (! isset($this->loaders[$extension])) {
throw InvalidConfigurationFormat::new($this->file);
}
return $this->loaders[$extension]($this->file)->getConfiguration();
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Configuration\Exception\FileNotFound;
use Doctrine\Migrations\Configuration\Migration\Exception\JsonNotValid;
use function assert;
use function file_exists;
use function file_get_contents;
use function json_decode;
use function json_last_error;
use const JSON_ERROR_NONE;
final class JsonFile extends ConfigurationFile
{
public function getConfiguration(): Configuration
{
if (! file_exists($this->file)) {
throw FileNotFound::new($this->file);
}
$contents = file_get_contents($this->file);
assert($contents !== false);
$config = json_decode($contents, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw JsonNotValid::new();
}
if (isset($config['migrations_paths'])) {
$config['migrations_paths'] = $this->getDirectoriesRelativeToFile(
$config['migrations_paths'],
$this->file,
);
}
return (new ConfigurationArray($config))->getConfiguration();
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Configuration\Exception\FileNotFound;
use function assert;
use function file_exists;
use function is_array;
final class PhpFile extends ConfigurationFile
{
public function getConfiguration(): Configuration
{
if (! file_exists($this->file)) {
throw FileNotFound::new($this->file);
}
$config = require $this->file;
if ($config instanceof Configuration) {
return $config;
}
assert(is_array($config));
if (isset($config['migrations_paths'])) {
$config['migrations_paths'] = $this->getDirectoriesRelativeToFile(
$config['migrations_paths'],
$this->file,
);
}
return (new ConfigurationArray($config))->getConfiguration();
}
}

View File

@@ -0,0 +1,73 @@
<xs:schema
attributeFormDefault="unqualified"
elementFormDefault="qualified"
targetNamespace="http://doctrine-project.org/schemas/migrations/configuration/3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="doctrine-migrations">
<xs:complexType>
<xs:all minOccurs="0">
<xs:element type="xs:string" name="name" minOccurs="0" maxOccurs="1"/>
<xs:element type="xs:string" name="custom-template" minOccurs="0" maxOccurs="1"/>
<xs:element name="migrations-paths" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence>
<xs:element name="path" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="namespace" type="xs:string" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="storage" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence>
<xs:any maxOccurs="1" processContents="lax"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="organize-migrations" minOccurs="0" maxOccurs="1">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="year" />
<xs:enumeration value="year_and_month" />
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element type="xs:string" name="connection" minOccurs="0" maxOccurs="1"/>
<xs:element type="xs:string" name="em" minOccurs="0" maxOccurs="1"/>
<xs:element type="xs:boolean" name="all-or-nothing" minOccurs="0" maxOccurs="1"/>
<xs:element type="xs:boolean" name="check-database-platform" minOccurs="0" maxOccurs="1"/>
<xs:element name="migrations" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="migration" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:all>
</xs:complexType>
</xs:element>
<xs:element name="table-storage">
<xs:complexType>
<xs:attribute name="table-name" type="xs:string" use="optional"/>
<xs:attribute name="version-column-name" type="xs:string" use="optional"/>
<xs:attribute name="version-column-length" type="xs:positiveInteger" use="optional"/>
<xs:attribute name="executed-at-column-name" type="xs:string" use="optional"/>
<xs:attribute name="execution-time-column-name" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Configuration\Exception\FileNotFound;
use Doctrine\Migrations\Configuration\Migration\Exception\XmlNotValid;
use Doctrine\Migrations\Tools\BooleanStringFormatter;
use DOMDocument;
use SimpleXMLElement;
use function assert;
use function file_exists;
use function file_get_contents;
use function libxml_clear_errors;
use function libxml_use_internal_errors;
use function simplexml_load_string;
use function strtr;
use const DIRECTORY_SEPARATOR;
use const LIBXML_NOCDATA;
final class XmlFile extends ConfigurationFile
{
public function getConfiguration(): Configuration
{
if (! file_exists($this->file)) {
throw FileNotFound::new($this->file);
}
$this->validateXml($this->file);
$rawXML = file_get_contents($this->file);
assert($rawXML !== false);
$root = simplexml_load_string($rawXML, SimpleXMLElement::class, LIBXML_NOCDATA);
assert($root !== false);
$config = $this->extractParameters($root, true);
if (isset($config['all_or_nothing'])) {
$config['all_or_nothing'] = BooleanStringFormatter::toBoolean(
$config['all_or_nothing'],
false,
);
}
if (isset($config['transactional'])) {
$config['transactional'] = BooleanStringFormatter::toBoolean(
$config['transactional'],
true,
);
}
if (isset($config['migrations_paths'])) {
$config['migrations_paths'] = $this->getDirectoriesRelativeToFile(
$config['migrations_paths'],
$this->file,
);
}
return (new ConfigurationArray($config))->getConfiguration();
}
/** @return mixed[] */
private function extractParameters(SimpleXMLElement $root, bool $loopOverNodes): array
{
$config = [];
$itemsToCheck = $loopOverNodes ? $root->children() : $root->attributes();
if (! ($itemsToCheck instanceof SimpleXMLElement)) {
return $config;
}
foreach ($itemsToCheck as $node) {
$nodeName = strtr($node->getName(), '-', '_');
if ($nodeName === 'migrations_paths') {
$config['migrations_paths'] = [];
foreach ($node->path as $pathNode) {
$config['migrations_paths'][(string) $pathNode['namespace']] = (string) $pathNode;
}
} elseif ($nodeName === 'storage' && $node->{'table-storage'} instanceof SimpleXMLElement) {
$config['table_storage'] = $this->extractParameters($node->{'table-storage'}, false);
} elseif ($nodeName === 'migrations') {
$config['migrations'] = $this->extractMigrations($node);
} else {
$config[$nodeName] = (string) $node;
}
}
return $config;
}
/** @return list<string> */
private function extractMigrations(SimpleXMLElement $node): array
{
$migrations = [];
foreach ($node->migration as $pathNode) {
$migrations[] = (string) $pathNode;
}
return $migrations;
}
private function validateXml(string $file): void
{
try {
libxml_use_internal_errors(true);
$xml = new DOMDocument();
if ($xml->load($file) === false) {
throw XmlNotValid::malformed();
}
$xsdPath = __DIR__ . DIRECTORY_SEPARATOR . 'XML' . DIRECTORY_SEPARATOR . 'configuration.xsd';
if ($xml->schemaValidate($xsdPath) === false) {
throw XmlNotValid::failedValidation();
}
} finally {
libxml_clear_errors();
libxml_use_internal_errors(false);
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Configuration\Migration;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Configuration\Exception\FileNotFound;
use Doctrine\Migrations\Configuration\Migration\Exception\YamlNotAvailable;
use Doctrine\Migrations\Configuration\Migration\Exception\YamlNotValid;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
use function assert;
use function class_exists;
use function file_exists;
use function file_get_contents;
use function is_array;
final class YamlFile extends ConfigurationFile
{
public function getConfiguration(): Configuration
{
if (! class_exists(Yaml::class)) {
throw YamlNotAvailable::new();
}
if (! file_exists($this->file)) {
throw FileNotFound::new($this->file);
}
$content = file_get_contents($this->file);
assert($content !== false);
try {
$config = Yaml::parse($content);
} catch (ParseException) {
throw YamlNotValid::malformed();
}
if (! is_array($config)) {
throw YamlNotValid::invalid();
}
if (isset($config['migrations_paths'])) {
$config['migrations_paths'] = $this->getDirectoriesRelativeToFile(
$config['migrations_paths'],
$this->file,
);
}
return (new ConfigurationArray($config))->getConfiguration();
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations;
use Doctrine\DBAL\Connection;
use Doctrine\Migrations\Exception\MigrationConfigurationConflict;
use Doctrine\Migrations\Metadata\MigrationPlanList;
use Doctrine\Migrations\Query\Query;
use Doctrine\Migrations\Tools\BytesFormatter;
use Doctrine\Migrations\Tools\TransactionHelper;
use Doctrine\Migrations\Version\Executor;
use Psr\Log\LoggerInterface;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Stopwatch\StopwatchEvent;
use Throwable;
use function count;
use const COUNT_RECURSIVE;
/**
* The DbalMigrator class is responsible for generating and executing the SQL for a migration.
*
* @internal
*/
class DbalMigrator implements Migrator
{
public function __construct(
private readonly Connection $connection,
private readonly EventDispatcher $dispatcher,
private readonly Executor $executor,
private readonly LoggerInterface $logger,
private readonly Stopwatch $stopwatch,
) {
}
/** @return array<string, Query[]> */
private function executeMigrations(
MigrationPlanList $migrationsPlan,
MigratorConfiguration $migratorConfiguration,
): array {
$allOrNothing = $migratorConfiguration->isAllOrNothing();
if ($allOrNothing) {
$this->assertAllMigrationsAreTransactional($migrationsPlan);
$this->connection->beginTransaction();
}
try {
$this->dispatcher->dispatchMigrationEvent(Events::onMigrationsMigrating, $migrationsPlan, $migratorConfiguration);
$sql = $this->executePlan($migrationsPlan, $migratorConfiguration);
$this->dispatcher->dispatchMigrationEvent(Events::onMigrationsMigrated, $migrationsPlan, $migratorConfiguration);
} catch (Throwable $e) {
if ($allOrNothing) {
TransactionHelper::rollbackIfInTransaction($this->connection);
}
throw $e;
}
if ($allOrNothing) {
TransactionHelper::commitIfInTransaction($this->connection);
}
return $sql;
}
private function assertAllMigrationsAreTransactional(MigrationPlanList $migrationsPlan): void
{
foreach ($migrationsPlan->getItems() as $plan) {
if (! $plan->getMigration()->isTransactional()) {
throw MigrationConfigurationConflict::migrationIsNotTransactional($plan->getMigration());
}
}
}
/** @return array<string, Query[]> */
private function executePlan(MigrationPlanList $migrationsPlan, MigratorConfiguration $migratorConfiguration): array
{
$sql = [];
foreach ($migrationsPlan->getItems() as $plan) {
$versionExecutionResult = $this->executor->execute($plan, $migratorConfiguration);
// capture the to Schema for the migration so we have the ability to use
// it as the from Schema for the next migration when we are running a dry run
// $toSchema may be null in the case of skipped migrations
if (! $versionExecutionResult->isSkipped()) {
$migratorConfiguration->setFromSchema($versionExecutionResult->getToSchema());
}
$sql[(string) $plan->getVersion()] = $versionExecutionResult->getSql();
}
return $sql;
}
/** @param array<string, Query[]> $sql */
private function endMigrations(
StopwatchEvent $stopwatchEvent,
MigrationPlanList $migrationsPlan,
array $sql,
): void {
$stopwatchEvent->stop();
$this->logger->notice(
'finished in {duration}ms, used {memory} memory, {migrations_count} migrations executed, {queries_count} sql queries',
[
'duration' => $stopwatchEvent->getDuration(),
'memory' => BytesFormatter::formatBytes($stopwatchEvent->getMemory()),
'migrations_count' => count($migrationsPlan),
'queries_count' => count($sql, COUNT_RECURSIVE) - count($sql),
],
);
}
/**
* {@inheritDoc}
*/
public function migrate(MigrationPlanList $migrationsPlan, MigratorConfiguration $migratorConfiguration): array
{
if (count($migrationsPlan) === 0) {
$this->logger->notice('No migrations to execute.');
return [];
}
$stopwatchEvent = $this->stopwatch->start('migrate');
$sql = $this->executeMigrations($migrationsPlan, $migratorConfiguration);
$this->endMigrations($stopwatchEvent, $migrationsPlan, $sql);
return $sql;
}
}

View File

@@ -0,0 +1,465 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Configuration\Connection\ConnectionLoader;
use Doctrine\Migrations\Configuration\EntityManager\EntityManagerLoader;
use Doctrine\Migrations\Configuration\Migration\ConfigurationLoader;
use Doctrine\Migrations\Exception\FrozenDependencies;
use Doctrine\Migrations\Exception\MissingDependency;
use Doctrine\Migrations\Finder\GlobFinder;
use Doctrine\Migrations\Finder\MigrationFinder;
use Doctrine\Migrations\Finder\RecursiveRegexFinder;
use Doctrine\Migrations\Generator\ClassNameGenerator;
use Doctrine\Migrations\Generator\ConcatenationFileBuilder;
use Doctrine\Migrations\Generator\DiffGenerator;
use Doctrine\Migrations\Generator\FileBuilder;
use Doctrine\Migrations\Generator\Generator;
use Doctrine\Migrations\Generator\SqlGenerator;
use Doctrine\Migrations\Metadata\Storage\MetadataStorage;
use Doctrine\Migrations\Metadata\Storage\TableMetadataStorage;
use Doctrine\Migrations\Metadata\Storage\TableMetadataStorageConfiguration;
use Doctrine\Migrations\Provider\DBALSchemaDiffProvider;
use Doctrine\Migrations\Provider\EmptySchemaProvider;
use Doctrine\Migrations\Provider\LazySchemaDiffProvider;
use Doctrine\Migrations\Provider\OrmSchemaProvider;
use Doctrine\Migrations\Provider\SchemaDiffProvider;
use Doctrine\Migrations\Provider\SchemaProvider;
use Doctrine\Migrations\Tools\Console\ConsoleInputMigratorConfigurationFactory;
use Doctrine\Migrations\Tools\Console\Helper\MigrationStatusInfosHelper;
use Doctrine\Migrations\Tools\Console\MigratorConfigurationFactory;
use Doctrine\Migrations\Version\AliasResolver;
use Doctrine\Migrations\Version\AlphabeticalComparator;
use Doctrine\Migrations\Version\Comparator;
use Doctrine\Migrations\Version\CurrentMigrationStatusCalculator;
use Doctrine\Migrations\Version\DbalExecutor;
use Doctrine\Migrations\Version\DbalMigrationFactory;
use Doctrine\Migrations\Version\DefaultAliasResolver;
use Doctrine\Migrations\Version\Executor;
use Doctrine\Migrations\Version\MigrationFactory;
use Doctrine\Migrations\Version\MigrationPlanCalculator;
use Doctrine\Migrations\Version\MigrationStatusCalculator;
use Doctrine\Migrations\Version\SortedMigrationPlanCalculator;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Stopwatch\Stopwatch;
use function array_key_exists;
use function call_user_func;
use function method_exists;
use function preg_quote;
use function sprintf;
/**
* The DependencyFactory is responsible for wiring up and managing internal class dependencies.
*/
class DependencyFactory
{
/** @var array<string, bool> */
private array $inResolution = [];
private Configuration|null $configuration = null;
/** @var object[]|callable[] */
private array $dependencies = [];
private Connection|null $connection = null;
private EntityManagerInterface|null $em = null;
private EventManager|null $eventManager = null;
private bool $frozen = false;
private ConfigurationLoader $configurationLoader;
private ConnectionLoader $connectionLoader;
private EntityManagerLoader|null $emLoader = null;
/** @var callable[] */
private array $factories = [];
public static function fromConnection(
ConfigurationLoader $configurationLoader,
ConnectionLoader $connectionLoader,
LoggerInterface|null $logger = null,
): self {
$dependencyFactory = new self($logger);
$dependencyFactory->configurationLoader = $configurationLoader;
$dependencyFactory->connectionLoader = $connectionLoader;
return $dependencyFactory;
}
public static function fromEntityManager(
ConfigurationLoader $configurationLoader,
EntityManagerLoader $emLoader,
LoggerInterface|null $logger = null,
): self {
$dependencyFactory = new self($logger);
$dependencyFactory->configurationLoader = $configurationLoader;
$dependencyFactory->emLoader = $emLoader;
return $dependencyFactory;
}
private function __construct(LoggerInterface|null $logger)
{
if ($logger === null) {
return;
}
$this->setDefinition(LoggerInterface::class, static fn (): LoggerInterface => $logger);
}
public function isFrozen(): bool
{
return $this->frozen;
}
public function freeze(): void
{
$this->frozen = true;
}
private function assertNotFrozen(): void
{
if ($this->frozen) {
throw FrozenDependencies::new();
}
}
public function hasEntityManager(): bool
{
return $this->emLoader !== null;
}
public function setConfigurationLoader(ConfigurationLoader $configurationLoader): void
{
$this->assertNotFrozen();
$this->configurationLoader = $configurationLoader;
}
public function getConfiguration(): Configuration
{
if ($this->configuration === null) {
$this->configuration = $this->configurationLoader->getConfiguration();
$this->frozen = true;
}
return $this->configuration;
}
public function getConnection(): Connection
{
if ($this->connection === null) {
$this->connection = $this->hasEntityManager()
? $this->getEntityManager()->getConnection()
: $this->connectionLoader->getConnection($this->getConfiguration()->getConnectionName());
$this->frozen = true;
}
return $this->connection;
}
public function getEntityManager(): EntityManagerInterface
{
if ($this->em === null) {
if ($this->emLoader === null) {
throw MissingDependency::noEntityManager();
}
$this->em = $this->emLoader->getEntityManager($this->getConfiguration()->getEntityManagerName());
$this->frozen = true;
}
return $this->em;
}
public function getVersionComparator(): Comparator
{
return $this->getDependency(Comparator::class, static fn (): AlphabeticalComparator => new AlphabeticalComparator());
}
public function getLogger(): LoggerInterface
{
return $this->getDependency(LoggerInterface::class, static fn (): LoggerInterface => new NullLogger());
}
public function getEventDispatcher(): EventDispatcher
{
return $this->getDependency(EventDispatcher::class, fn (): EventDispatcher => new EventDispatcher(
$this->getConnection(),
$this->getEventManager(),
));
}
public function getClassNameGenerator(): ClassNameGenerator
{
return $this->getDependency(ClassNameGenerator::class, static fn (): ClassNameGenerator => new ClassNameGenerator());
}
public function getSchemaDumper(): SchemaDumper
{
return $this->getDependency(SchemaDumper::class, function (): SchemaDumper {
$excludedTables = [];
$metadataConfig = $this->getConfiguration()->getMetadataStorageConfiguration();
if ($metadataConfig instanceof TableMetadataStorageConfiguration) {
$excludedTables[] = sprintf('/^%s$/', preg_quote($metadataConfig->getTableName(), '/'));
}
return new SchemaDumper(
$this->getConnection()->getDatabasePlatform(),
$this->getConnection()->createSchemaManager(),
$this->getMigrationGenerator(),
$this->getMigrationSqlGenerator(),
$excludedTables,
);
});
}
private function getEmptySchemaProvider(): SchemaProvider
{
return $this->getDependency(EmptySchemaProvider::class, fn (): SchemaProvider => new EmptySchemaProvider($this->getConnection()->createSchemaManager()));
}
public function hasSchemaProvider(): bool
{
try {
$this->getSchemaProvider();
} catch (MissingDependency) {
return false;
}
return true;
}
public function getSchemaProvider(): SchemaProvider
{
return $this->getDependency(SchemaProvider::class, function (): SchemaProvider {
if ($this->hasEntityManager()) {
return new OrmSchemaProvider($this->getEntityManager());
}
throw MissingDependency::noSchemaProvider();
});
}
public function getDiffGenerator(): DiffGenerator
{
return $this->getDependency(DiffGenerator::class, fn (): DiffGenerator => new DiffGenerator(
$this->getConnection()->getConfiguration(),
$this->getConnection()->createSchemaManager(),
$this->getSchemaProvider(),
$this->getConnection()->getDatabasePlatform(),
$this->getMigrationGenerator(),
$this->getMigrationSqlGenerator(),
$this->getEmptySchemaProvider(),
));
}
public function getSchemaDiffProvider(): SchemaDiffProvider
{
return $this->getDependency(SchemaDiffProvider::class, fn (): LazySchemaDiffProvider => new LazySchemaDiffProvider(
new DBALSchemaDiffProvider(
$this->getConnection()->createSchemaManager(),
$this->getConnection()->getDatabasePlatform(),
),
));
}
private function getFileBuilder(): FileBuilder
{
return $this->getDependency(FileBuilder::class, static fn (): FileBuilder => new ConcatenationFileBuilder());
}
private function getParameterFormatter(): ParameterFormatter
{
return $this->getDependency(ParameterFormatter::class, fn (): ParameterFormatter => new InlineParameterFormatter($this->getConnection()));
}
public function getMigrationsFinder(): MigrationFinder
{
return $this->getDependency(MigrationFinder::class, function (): MigrationFinder {
$configs = $this->getConfiguration();
$needsRecursiveFinder = $configs->areMigrationsOrganizedByYear() || $configs->areMigrationsOrganizedByYearAndMonth();
return $needsRecursiveFinder ? new RecursiveRegexFinder() : new GlobFinder();
});
}
public function getMigrationRepository(): MigrationsRepository
{
return $this->getDependency(MigrationsRepository::class, fn (): MigrationsRepository => new FilesystemMigrationsRepository(
$this->getConfiguration()->getMigrationClasses(),
$this->getConfiguration()->getMigrationDirectories(),
$this->getMigrationsFinder(),
$this->getMigrationFactory(),
));
}
public function getMigrationFactory(): MigrationFactory
{
return $this->getDependency(MigrationFactory::class, fn (): MigrationFactory => new DbalMigrationFactory($this->getConnection(), $this->getLogger()));
}
public function setService(string $id, object|callable $service): void
{
$this->assertNotFrozen();
$this->dependencies[$id] = $service;
}
public function getMetadataStorage(): MetadataStorage
{
return $this->getDependency(MetadataStorage::class, fn (): MetadataStorage => new TableMetadataStorage(
$this->getConnection(),
$this->getVersionComparator(),
$this->getConfiguration()->getMetadataStorageConfiguration(),
$this->getMigrationRepository(),
));
}
private function getVersionExecutor(): Executor
{
return $this->getDependency(Executor::class, fn (): Executor => new DbalExecutor(
$this->getMetadataStorage(),
$this->getEventDispatcher(),
$this->getConnection(),
$this->getSchemaDiffProvider(),
$this->getLogger(),
$this->getParameterFormatter(),
$this->getStopwatch(),
));
}
public function getQueryWriter(): QueryWriter
{
return $this->getDependency(QueryWriter::class, fn (): QueryWriter => new FileQueryWriter(
$this->getFileBuilder(),
$this->getLogger(),
));
}
public function getVersionAliasResolver(): AliasResolver
{
return $this->getDependency(AliasResolver::class, fn (): AliasResolver => new DefaultAliasResolver(
$this->getMigrationPlanCalculator(),
$this->getMetadataStorage(),
$this->getMigrationStatusCalculator(),
));
}
public function getMigrationStatusCalculator(): MigrationStatusCalculator
{
return $this->getDependency(MigrationStatusCalculator::class, fn (): MigrationStatusCalculator => new CurrentMigrationStatusCalculator(
$this->getMigrationPlanCalculator(),
$this->getMetadataStorage(),
));
}
public function getMigrationPlanCalculator(): MigrationPlanCalculator
{
return $this->getDependency(MigrationPlanCalculator::class, fn (): MigrationPlanCalculator => new SortedMigrationPlanCalculator(
$this->getMigrationRepository(),
$this->getMetadataStorage(),
$this->getVersionComparator(),
));
}
public function getMigrationGenerator(): Generator
{
return $this->getDependency(Generator::class, fn (): Generator => new Generator($this->getConfiguration()));
}
public function getMigrationSqlGenerator(): SqlGenerator
{
return $this->getDependency(SqlGenerator::class, fn (): SqlGenerator => new SqlGenerator(
$this->getConfiguration(),
$this->getConnection()->getDatabasePlatform(),
));
}
public function getConsoleInputMigratorConfigurationFactory(): MigratorConfigurationFactory
{
return $this->getDependency(MigratorConfigurationFactory::class, fn (): MigratorConfigurationFactory => new ConsoleInputMigratorConfigurationFactory(
$this->getConfiguration(),
));
}
public function getMigrationStatusInfosHelper(): MigrationStatusInfosHelper
{
return $this->getDependency(MigrationStatusInfosHelper::class, fn (): MigrationStatusInfosHelper => new MigrationStatusInfosHelper(
$this->getConfiguration(),
$this->getConnection(),
$this->getVersionAliasResolver(),
$this->getMigrationPlanCalculator(),
$this->getMigrationStatusCalculator(),
$this->getMetadataStorage(),
));
}
public function getMigrator(): Migrator
{
return $this->getDependency(Migrator::class, fn (): Migrator => new DbalMigrator(
$this->getConnection(),
$this->getEventDispatcher(),
$this->getVersionExecutor(),
$this->getLogger(),
$this->getStopwatch(),
));
}
public function getStopwatch(): Stopwatch
{
return $this->getDependency(Stopwatch::class, static fn (): Stopwatch => new Stopwatch(true));
}
public function getRollup(): Rollup
{
return $this->getDependency(Rollup::class, fn (): Rollup => new Rollup(
$this->getMetadataStorage(),
$this->getMigrationRepository(),
));
}
private function getDependency(string $id, callable $callback): mixed
{
if (! isset($this->inResolution[$id]) && array_key_exists($id, $this->factories) && ! array_key_exists($id, $this->dependencies)) {
$this->inResolution[$id] = true;
$this->dependencies[$id] = call_user_func($this->factories[$id], $this);
unset($this->inResolution);
}
if (! array_key_exists($id, $this->dependencies)) {
$this->dependencies[$id] = $callback();
}
return $this->dependencies[$id];
}
public function setDefinition(string $id, callable $service): void
{
$this->assertNotFrozen();
$this->factories[$id] = $service;
}
private function getEventManager(): EventManager
{
if ($this->eventManager !== null) {
return $this->eventManager;
}
if ($this->hasEntityManager()) {
return $this->eventManager = $this->getEntityManager()->getEventManager();
}
if (method_exists(Connection::class, 'getEventManager')) {
// DBAL < 4
return $this->eventManager = $this->getConnection()->getEventManager();
}
return $this->eventManager = new EventManager();
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Event\Listeners;
use Doctrine\Common\EventSubscriber;
use Doctrine\Migrations\Event\MigrationsEventArgs;
use Doctrine\Migrations\Events;
use Doctrine\Migrations\Tools\TransactionHelper;
/**
* Listens for `onMigrationsMigrated` and, if the connection has autocommit
* makes sure to do the final commit to ensure changes stick around.
*
* @internal
*/
final class AutoCommitListener implements EventSubscriber
{
public function onMigrationsMigrated(MigrationsEventArgs $args): void
{
$conn = $args->getConnection();
$conf = $args->getMigratorConfiguration();
if ($conf->isDryRun() || $conn->isAutoCommit()) {
return;
}
TransactionHelper::commitIfInTransaction($conn);
}
/** {@inheritDoc} */
public function getSubscribedEvents()
{
return [Events::onMigrationsMigrated];
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Event;
use Doctrine\Common\EventArgs;
use Doctrine\DBAL\Connection;
use Doctrine\Migrations\Metadata\MigrationPlanList;
use Doctrine\Migrations\MigratorConfiguration;
/**
* The MigrationEventsArgs class is passed to events not related to a single migration version.
*/
final class MigrationsEventArgs extends EventArgs
{
public function __construct(
private readonly Connection $connection,
private readonly MigrationPlanList $plan,
private readonly MigratorConfiguration $migratorConfiguration,
) {
}
public function getConnection(): Connection
{
return $this->connection;
}
public function getPlan(): MigrationPlanList
{
return $this->plan;
}
public function getMigratorConfiguration(): MigratorConfiguration
{
return $this->migratorConfiguration;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Event;
use Doctrine\Common\EventArgs;
use Doctrine\DBAL\Connection;
use Doctrine\Migrations\Metadata\MigrationPlan;
use Doctrine\Migrations\MigratorConfiguration;
/**
* The MigrationsVersionEventArgs class is passed to events related to a single migration version.
*/
final class MigrationsVersionEventArgs extends EventArgs
{
public function __construct(
private readonly Connection $connection,
private readonly MigrationPlan $plan,
private readonly MigratorConfiguration $migratorConfiguration,
) {
}
public function getConnection(): Connection
{
return $this->connection;
}
public function getPlan(): MigrationPlan
{
return $this->plan;
}
public function getMigratorConfiguration(): MigratorConfiguration
{
return $this->migratorConfiguration;
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations;
use Doctrine\Common\EventArgs;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\Migrations\Event\MigrationsEventArgs;
use Doctrine\Migrations\Event\MigrationsVersionEventArgs;
use Doctrine\Migrations\Metadata\MigrationPlan;
use Doctrine\Migrations\Metadata\MigrationPlanList;
/**
* The EventDispatcher class is responsible for dispatching events internally that a user can listen for.
*
* @internal
*/
final class EventDispatcher
{
public function __construct(
private readonly Connection $connection,
private readonly EventManager $eventManager,
) {
}
public function dispatchMigrationEvent(
string $eventName,
MigrationPlanList $migrationsPlan,
MigratorConfiguration $migratorConfiguration,
): void {
$event = $this->createMigrationEventArgs($migrationsPlan, $migratorConfiguration);
$this->dispatchEvent($eventName, $event);
}
public function dispatchVersionEvent(
string $eventName,
MigrationPlan $plan,
MigratorConfiguration $migratorConfiguration,
): void {
$event = $this->createMigrationsVersionEventArgs(
$plan,
$migratorConfiguration,
);
$this->dispatchEvent($eventName, $event);
}
private function dispatchEvent(string $eventName, EventArgs|null $args = null): void
{
$this->eventManager->dispatchEvent($eventName, $args);
}
private function createMigrationEventArgs(
MigrationPlanList $migrationsPlan,
MigratorConfiguration $migratorConfiguration,
): MigrationsEventArgs {
return new MigrationsEventArgs($this->connection, $migrationsPlan, $migratorConfiguration);
}
private function createMigrationsVersionEventArgs(
MigrationPlan $plan,
MigratorConfiguration $migratorConfiguration,
): MigrationsVersionEventArgs {
return new MigrationsVersionEventArgs(
$this->connection,
$plan,
$migratorConfiguration,
);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations;
/**
* The Events class contains constants for event names that a user can subscribe to.
*/
final class Events
{
public const onMigrationsMigrating = 'onMigrationsMigrating';
public const onMigrationsMigrated = 'onMigrationsMigrated';
public const onMigrationsVersionExecuting = 'onMigrationsVersionExecuting';
public const onMigrationsVersionExecuted = 'onMigrationsVersionExecuted';
public const onMigrationsVersionSkipped = 'onMigrationsVersionSkipped';
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
final class AbortMigration extends RuntimeException implements ControlException
{
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
use function sprintf;
final class AlreadyAtVersion extends RuntimeException implements MigrationException
{
public static function new(string $version): self
{
return new self(
sprintf(
'Database is already at version %s',
$version,
),
6,
);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
interface ControlException extends MigrationException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
interface DependencyException extends MigrationException
{
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
use function sprintf;
final class DuplicateMigrationVersion extends RuntimeException implements MigrationException
{
public static function new(string $version, string $class): self
{
return new self(
sprintf(
'Migration version %s already registered with class %s',
$version,
$class,
),
7,
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use LogicException;
final class FrozenDependencies extends LogicException implements DependencyException
{
public static function new(): self
{
return new self('The dependencies are frozen and cannot be edited anymore.');
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use LogicException;
final class FrozenMigration extends LogicException implements MigrationException
{
public static function new(): self
{
return new self('The migration is frozen and cannot be edited anymore.');
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
final class IrreversibleMigration extends RuntimeException implements MigrationException
{
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
final class MetadataStorageError extends RuntimeException implements MigrationException
{
public static function notUpToDate(): self
{
return new self('The metadata storage is not up to date, please run the sync-metadata-storage command to fix this issue.');
}
public static function notInitialized(): self
{
return new self('The metadata storage is not initialized, please run the sync-metadata-storage command to fix this issue.');
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
use function sprintf;
final class MigrationClassNotFound extends RuntimeException implements MigrationException
{
public static function new(string $migrationClass): self
{
return new self(
sprintf(
'Migration class "%s" was not found?',
$migrationClass,
),
);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use Doctrine\Migrations\AbstractMigration;
use UnexpectedValueException;
use function get_debug_type;
use function sprintf;
final class MigrationConfigurationConflict extends UnexpectedValueException implements MigrationException
{
public static function migrationIsNotTransactional(AbstractMigration $migration): self
{
return new self(sprintf(
<<<'EXCEPTION'
Context: attempting to execute migrations with all-or-nothing enabled
Problem: migration %s is marked as non-transactional
Solution: disable all-or-nothing in configuration or by command-line option, or enable transactions for all migrations
EXCEPTION,
get_debug_type($migration),
));
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use Throwable;
interface MigrationException extends Throwable
{
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use Doctrine\Migrations\Version\Version;
use RuntimeException;
use function sprintf;
final class MigrationNotAvailable extends RuntimeException implements MigrationException
{
public static function forVersion(Version $version): self
{
return new self(
sprintf(
'The migration %s is not available',
(string) $version,
),
5,
);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
use function sprintf;
final class MigrationNotExecuted extends RuntimeException implements MigrationException
{
public static function new(string $version): self
{
return new self(
sprintf(
'The provided migration %s has not been executed',
$version,
),
5,
);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
final class MissingDependency extends RuntimeException implements DependencyException
{
public static function noEntityManager(): self
{
return new self('The entity manager is not available.');
}
public static function noSchemaProvider(): self
{
return new self('The schema provider is not available.');
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
use function sprintf;
final class NoMigrationsFoundWithCriteria extends RuntimeException implements MigrationException
{
public static function new(string|null $criteria = null): self
{
return new self(
$criteria !== null
? sprintf('Could not find any migrations matching your criteria (%s).', $criteria)
: 'Could not find any migrations matching your criteria.',
4,
);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
use Throwable;
final class NoMigrationsToExecute extends RuntimeException implements MigrationException
{
public static function new(Throwable|null $previous = null): self
{
return new self(
'Could not find any migrations to execute.',
4,
$previous,
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
final class NoTablesFound extends RuntimeException implements MigrationException
{
public static function new(): self
{
return new self('Your database schema does not contain any tables.');
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
final class PlanAlreadyExecuted extends RuntimeException implements MigrationException
{
public static function new(): self
{
return new self('This plan was already marked as executed.');
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
final class RollupFailed extends RuntimeException implements MigrationException
{
public static function noMigrationsFound(): self
{
return new self('No migrations found.');
}
public static function tooManyMigrations(): self
{
return new self('Too many migrations.');
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
final class SkipMigration extends RuntimeException implements ControlException
{
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Exception;
use RuntimeException;
use function sprintf;
final class UnknownMigrationVersion extends RuntimeException implements MigrationException
{
public static function new(string $version): self
{
return new self(
sprintf(
'Could not find migration version %s',
$version,
),
5,
);
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\Migrations\Generator\FileBuilder;
use Doctrine\Migrations\Query\Query;
use Psr\Log\LoggerInterface;
use function file_put_contents;
use function is_dir;
use function realpath;
/**
* The FileQueryWriter class is responsible for writing migration SQL queries to a file on disk.
*
* @internal
*/
final class FileQueryWriter implements QueryWriter
{
public function __construct(
private readonly FileBuilder $migrationFileBuilder,
private readonly LoggerInterface $logger,
) {
}
/** @param array<string,Query[]> $queriesByVersion */
public function write(
string $path,
string $direction,
array $queriesByVersion,
DateTimeInterface|null $now = null,
): bool {
$now ??= new DateTimeImmutable();
$string = $this->migrationFileBuilder
->buildMigrationFile($queriesByVersion, $direction, $now);
$path = $this->buildMigrationFilePath($path, $now);
$this->logger->info('Writing migration file to "{path}"', ['path' => $path]);
return file_put_contents($path, $string) !== false;
}
private function buildMigrationFilePath(string $path, DateTimeInterface $now): string
{
if (is_dir($path)) {
$path = realpath($path);
$path .= '/doctrine_migration_' . $now->format('YmdHis') . '.sql';
}
return $path;
}
}

View File

@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations;
use Doctrine\Migrations\Exception\DuplicateMigrationVersion;
use Doctrine\Migrations\Exception\MigrationClassNotFound;
use Doctrine\Migrations\Exception\MigrationException;
use Doctrine\Migrations\Finder\MigrationFinder;
use Doctrine\Migrations\Metadata\AvailableMigration;
use Doctrine\Migrations\Metadata\AvailableMigrationsSet;
use Doctrine\Migrations\Version\MigrationFactory;
use Doctrine\Migrations\Version\Version;
use function class_exists;
/**
* The FilesystemMigrationsRepository class is responsible for retrieving migrations, determining what the current migration
* version, etc.
*
* @internal
*/
class FilesystemMigrationsRepository implements MigrationsRepository
{
private bool $migrationsLoaded = false;
/** @var AvailableMigration[] */
private array $migrations = [];
/**
* @param string[] $classes
* @param array<string, string> $migrationDirectories
*/
public function __construct(
array $classes,
private readonly array $migrationDirectories,
private readonly MigrationFinder $migrationFinder,
private readonly MigrationFactory $versionFactory,
) {
$this->registerMigrations($classes);
}
private function registerMigrationInstance(Version $version, AbstractMigration $migration): AvailableMigration
{
if (isset($this->migrations[(string) $version])) {
throw DuplicateMigrationVersion::new(
(string) $version,
(string) $version,
);
}
$this->migrations[(string) $version] = new AvailableMigration($version, $migration);
return $this->migrations[(string) $version];
}
/** @throws MigrationException */
public function registerMigration(string $migrationClassName): AvailableMigration
{
$this->ensureMigrationClassExists($migrationClassName);
$version = new Version($migrationClassName);
$migration = $this->versionFactory->createVersion($migrationClassName);
return $this->registerMigrationInstance($version, $migration);
}
/**
* @param string[] $migrations
*
* @return AvailableMigration[]
*/
private function registerMigrations(array $migrations): array
{
$versions = [];
foreach ($migrations as $class) {
$versions[] = $this->registerMigration($class);
}
return $versions;
}
public function hasMigration(string $version): bool
{
$this->loadMigrationsFromDirectories();
return isset($this->migrations[$version]);
}
public function getMigration(Version $version): AvailableMigration
{
$this->loadMigrationsFromDirectories();
if (! isset($this->migrations[(string) $version])) {
throw MigrationClassNotFound::new((string) $version);
}
return $this->migrations[(string) $version];
}
/**
* Returns a non-sorted set of migrations.
*/
public function getMigrations(): AvailableMigrationsSet
{
$this->loadMigrationsFromDirectories();
return new AvailableMigrationsSet($this->migrations);
}
/** @throws MigrationException */
private function ensureMigrationClassExists(string $class): void
{
if (! class_exists($class)) {
throw MigrationClassNotFound::new($class);
}
}
private function loadMigrationsFromDirectories(): void
{
$migrationDirectories = $this->migrationDirectories;
if ($this->migrationsLoaded) {
return;
}
$this->migrationsLoaded = true;
foreach ($migrationDirectories as $namespace => $path) {
$migrations = $this->migrationFinder->findMigrations(
$path,
$namespace,
);
$this->registerMigrations($migrations);
}
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Finder\Exception;
interface FinderException
{
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Finder\Exception;
use InvalidArgumentException;
use function sprintf;
final class InvalidDirectory extends InvalidArgumentException implements FinderException
{
public static function new(string $directory): self
{
return new self(sprintf('Cannot load migrations from "%s" because it is not a valid directory', $directory));
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Finder\Exception;
use InvalidArgumentException;
use function sprintf;
use const PHP_EOL;
final class NameIsReserved extends InvalidArgumentException implements FinderException
{
public static function new(string $version): self
{
return new self(sprintf(
'Cannot load a migrations with the name "%s" because it is reserved by Doctrine Migrations.'
. PHP_EOL
. 'It is used to revert all migrations including the first one.',
$version,
));
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Finder;
use Doctrine\Migrations\Finder\Exception\InvalidDirectory;
use Doctrine\Migrations\Finder\Exception\NameIsReserved;
use ReflectionClass;
use function assert;
use function get_declared_classes;
use function in_array;
use function is_dir;
use function realpath;
use function strlen;
use function strncmp;
/**
* The Finder class is responsible for for finding migrations on disk at a given path.
*/
abstract class Finder implements MigrationFinder
{
protected static function requireOnce(string $path): void
{
require_once $path;
}
/** @throws InvalidDirectory */
protected function getRealPath(string $directory): string
{
$dir = realpath($directory);
if ($dir === false || ! is_dir($dir)) {
throw InvalidDirectory::new($directory);
}
return $dir;
}
/**
* @param string[] $files
*
* @return string[]
*
* @throws NameIsReserved
*/
protected function loadMigrations(array $files, string|null $namespace): array
{
$includedFiles = [];
foreach ($files as $file) {
static::requireOnce($file);
$realFile = realpath($file);
assert($realFile !== false);
$includedFiles[] = $realFile;
}
$classes = $this->loadMigrationClasses($includedFiles, $namespace);
$versions = [];
foreach ($classes as $class) {
$versions[] = $class->getName();
}
return $versions;
}
/**
* Look up all declared classes and find those classes contained
* in the given `$files` array.
*
* @param string[] $files The set of files that were `required`
* @param string|null $namespace If not null only classes in this namespace will be returned
*
* @return ReflectionClass<object>[] the classes in `$files`
*/
protected function loadMigrationClasses(array $files, string|null $namespace = null): array
{
$classes = [];
foreach (get_declared_classes() as $class) {
$reflectionClass = new ReflectionClass($class);
if (! in_array($reflectionClass->getFileName(), $files, true)) {
continue;
}
if ($namespace !== null && ! $this->isReflectionClassInNamespace($reflectionClass, $namespace)) {
continue;
}
$classes[] = $reflectionClass;
}
return $classes;
}
/** @param ReflectionClass<object> $reflectionClass */
private function isReflectionClassInNamespace(ReflectionClass $reflectionClass, string $namespace): bool
{
return strncmp($reflectionClass->getName(), $namespace . '\\', strlen($namespace) + 1) === 0;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Finder;
use function glob;
use function rtrim;
/**
* The GlobFinder class finds migrations in a directory using the PHP glob() function.
*/
final class GlobFinder extends Finder
{
/**
* {@inheritDoc}
*/
public function findMigrations(string $directory, string|null $namespace = null): array
{
$dir = $this->getRealPath($directory);
$files = glob(rtrim($dir, '/') . '/Version*.php');
if ($files === false) {
$files = [];
}
return $this->loadMigrations($files, $namespace);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Finder;
/**
* The MigrationFinder interface defines the interface used for finding migrations in a given directory and namespace.
*/
interface MigrationFinder
{
/**
* @param string $directory The directory which the finder should search
* @param string|null $namespace If not null only classes in this namespace will be returned
*
* @return string[]
*/
public function findMigrations(string $directory, string|null $namespace = null): array;
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Finder;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use Traversable;
use function sprintf;
use const DIRECTORY_SEPARATOR;
/**
* The RecursiveRegexFinder class recursively searches the given directory for migrations.
*/
final class RecursiveRegexFinder extends Finder
{
private string $pattern;
public function __construct(string|null $pattern = null)
{
$this->pattern = $pattern ?? sprintf(
'#^.+\\%s[^\\%s]+\\.php$#i',
DIRECTORY_SEPARATOR,
DIRECTORY_SEPARATOR,
);
}
/** @return string[] */
public function findMigrations(string $directory, string|null $namespace = null): array
{
$dir = $this->getRealPath($directory);
return $this->loadMigrations(
$this->getMatches($this->createIterator($dir)),
$namespace,
);
}
/** @return RegexIterator<mixed, mixed, Traversable<mixed, mixed>> */
private function createIterator(string $dir): RegexIterator
{
/** @phpstan-ignore return.type (https://github.com/phpstan/phpstan/issues/13325) */
return new RegexIterator(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS),
RecursiveIteratorIterator::LEAVES_ONLY,
),
$this->getPattern(),
RegexIterator::GET_MATCH,
);
}
private function getPattern(): string
{
return $this->pattern;
}
/**
* @param RegexIterator<mixed, mixed, Traversable<mixed, mixed>> $iteratorFilesMatch
*
* @return string[]
*/
private function getMatches(RegexIterator $iteratorFilesMatch): array
{
$files = [];
foreach ($iteratorFilesMatch as $file) {
$files[] = $file[0];
}
return $files;
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Generator;
use DateTimeImmutable;
use DateTimeZone;
/*final */class ClassNameGenerator
{
public const VERSION_FORMAT = 'YmdHis';
public function generateClassName(string $namespace): string
{
return $namespace . '\\Version' . $this->generateVersionNumber();
}
private function generateVersionNumber(): string
{
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
return $now->format(self::VERSION_FORMAT);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Generator;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\Migrations\Query\Query;
use function sprintf;
/**
* The ConcatenationFileBuilder class is responsible for building a migration SQL file from an array of queries per version.
*
* @internal
*/
final class ConcatenationFileBuilder implements FileBuilder
{
/** @param array<string,Query[]> $queriesByVersion */
public function buildMigrationFile(
array $queriesByVersion,
string $direction,
DateTimeInterface|null $now = null,
): string {
$now ??= new DateTimeImmutable();
$string = sprintf("-- Doctrine Migration File Generated on %s\n", $now->format('Y-m-d H:i:s'));
foreach ($queriesByVersion as $version => $queries) {
$string .= "\n-- Version " . $version . "\n";
foreach ($queries as $query) {
$string .= $query->getStatement() . ";\n";
}
}
return $string;
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Generator;
use Doctrine\DBAL\Configuration as DBALConfiguration;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractAsset;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\ComparatorConfig;
use Doctrine\DBAL\Schema\NamedObject;
use Doctrine\DBAL\Schema\OptionallyNamedObject;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\Generator\Exception\NoChangesDetected;
use Doctrine\Migrations\Provider\SchemaProvider;
use function class_exists;
use function method_exists;
use function preg_match;
/**
* The DiffGenerator class is responsible for comparing two Doctrine\DBAL\Schema\Schema instances and generating a
* migration class with the SQL statements needed to migrate from one schema to the other.
*
* @internal
*/
class DiffGenerator
{
/** @param AbstractSchemaManager<AbstractPlatform> $schemaManager */
public function __construct(
private readonly DBALConfiguration $dbalConfiguration,
private readonly AbstractSchemaManager $schemaManager,
private readonly SchemaProvider $schemaProvider,
private readonly AbstractPlatform $platform,
private readonly Generator $migrationGenerator,
private readonly SqlGenerator $migrationSqlGenerator,
private readonly SchemaProvider $emptySchemaProvider,
) {
}
/** @throws NoChangesDetected */
public function generate(
string $fqcn,
string|null $filterExpression,
bool $formatted = false,
bool|null $nowdocOutput = null,
int $lineLength = 120,
bool $checkDbPlatform = true,
bool $fromEmptySchema = false,
): string {
if ($filterExpression !== null) {
$this->dbalConfiguration->setSchemaAssetsFilter(
static function ($assetName) use ($filterExpression) {
if ($assetName instanceof NamedObject || $assetName instanceof OptionallyNamedObject) {
if ($assetName->getObjectName() === null) {
return false;
}
$assetName = $assetName->getObjectName()->toString();
} elseif ($assetName instanceof AbstractAsset) {
/** @phpstan-ignore method.deprecated */
$assetName = $assetName->getName();
}
return preg_match($filterExpression, $assetName);
},
);
}
$fromSchema = $fromEmptySchema
? $this->createEmptySchema()
: $this->createFromSchema();
$toSchema = $this->createToSchema();
// prior to DBAL 4.0, the schema name was set to the first element in the search path,
// which is not necessarily the default schema name
if (
! method_exists($this->schemaManager, 'getSchemaSearchPaths')
&& $this->platform->supportsSchemas()
) {
/** @phpstan-ignore method.deprecated */
$defaultNamespace = $toSchema->getName();
if ($defaultNamespace !== '') {
/* @phpstan-ignore method.deprecated */
$toSchema->createNamespace($defaultNamespace);
}
}
if (class_exists(ComparatorConfig::class)) {
$comparator = $this->schemaManager->createComparator((new ComparatorConfig())->withReportModifiedIndexes(false));
} else {
$comparator = $this->schemaManager->createComparator();
}
$upSql = $this->platform->getAlterSchemaSQL($comparator->compareSchemas($fromSchema, $toSchema));
$up = $this->migrationSqlGenerator->generate(
$upSql,
$formatted,
$nowdocOutput,
$lineLength,
$checkDbPlatform,
);
$downSql = $this->platform->getAlterSchemaSQL($comparator->compareSchemas($toSchema, $fromSchema));
$down = $this->migrationSqlGenerator->generate(
$downSql,
$formatted,
$nowdocOutput,
$lineLength,
$checkDbPlatform,
);
if ($up === '' && $down === '') {
throw NoChangesDetected::new();
}
return $this->migrationGenerator->generateMigration(
$fqcn,
$up,
$down,
);
}
private function createEmptySchema(): Schema
{
return $this->emptySchemaProvider->createSchema();
}
private function createFromSchema(): Schema
{
return $this->schemaManager->introspectSchema();
}
private function createToSchema(): Schema
{
$toSchema = $this->schemaProvider->createSchema();
$schemaAssetsFilter = $this->dbalConfiguration->getSchemaAssetsFilter();
if ($schemaAssetsFilter !== null) {
foreach ($toSchema->getTables() as $table) {
/** @phpstan-ignore instanceof.alwaysTrue */
if ($table instanceof NamedObject) {
$tableName = $table->getObjectName()->toString();
} else {
$tableName = $table->getName();
}
if ($schemaAssetsFilter($tableName)) {
continue;
}
$toSchema->dropTable($tableName);
}
}
return $toSchema;
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Generator\Exception;
use Doctrine\Migrations\Exception\MigrationException;
interface GeneratorException extends MigrationException
{
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Generator\Exception;
use InvalidArgumentException;
use function sprintf;
final class InvalidTemplateSpecified extends InvalidArgumentException implements GeneratorException
{
public static function notFoundOrNotReadable(string $path): self
{
return new self(sprintf('The specified template "%s" cannot be found or is not readable.', $path));
}
public static function notReadable(string $path): self
{
return new self(sprintf('The specified template "%s" could not be read.', $path));
}
public static function empty(string $path): self
{
return new self(sprintf('The specified template "%s" is empty.', $path));
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Generator\Exception;
use RuntimeException;
final class NoChangesDetected extends RuntimeException implements GeneratorException
{
public static function new(): self
{
return new self('No changes detected in your mapping information.');
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Generator;
use DateTimeInterface;
use Doctrine\Migrations\Query\Query;
/**
* The ConcatenationFileBuilder class is responsible for building a migration SQL file from an array of queries per version.
*
* @internal
*/
interface FileBuilder
{
/** @param array<string,Query[]> $queriesByVersion */
public function buildMigrationFile(array $queriesByVersion, string $direction, DateTimeInterface|null $now = null): string;
}

View File

@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Generator;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Generator\Exception\InvalidTemplateSpecified;
use Doctrine\Migrations\Tools\Console\Helper\MigrationDirectoryHelper;
use InvalidArgumentException;
use function explode;
use function file_get_contents;
use function file_put_contents;
use function implode;
use function is_file;
use function is_readable;
use function preg_match;
use function preg_replace;
use function sprintf;
use function strtr;
use function trim;
/**
* The Generator class is responsible for generating a migration class.
*
* @internal
*/
class Generator
{
private const MIGRATION_TEMPLATE = <<<'TEMPLATE'
<?php
declare(strict_types=1);
namespace <namespace>;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class <className> extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
<up>
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
<down>
}<override>
}
TEMPLATE;
private string|null $template = null;
public function __construct(private readonly Configuration $configuration)
{
}
public function generateMigration(
string $fqcn,
string|null $up = null,
string|null $down = null,
): string {
$mch = [];
if (preg_match('~(.*)\\\\([^\\\\]+)~', $fqcn, $mch) !== 1) {
throw new InvalidArgumentException(sprintf('Invalid FQCN'));
}
[$fqcn, $namespace, $className] = $mch;
$dirs = $this->configuration->getMigrationDirectories();
if (! isset($dirs[$namespace])) {
throw new InvalidArgumentException(sprintf('Path not defined for the namespace "%s"', $namespace));
}
$dir = $dirs[$namespace];
$replacements = [
'<namespace>' => $namespace,
'<className>' => $className,
'<up>' => $up !== null ? ' ' . implode("\n ", explode("\n", $up)) : null,
'<down>' => $down !== null ? ' ' . implode("\n ", explode("\n", $down)) : null,
'<override>' => $this->configuration->isTransactional() ? '' : <<<'METHOD'
public function isTransactional(): bool
{
return false;
}
METHOD
,
];
$code = strtr($this->getTemplate(), $replacements);
$code = preg_replace('/^ +$/m', '', $code);
$directoryHelper = new MigrationDirectoryHelper();
$dir = $directoryHelper->getMigrationDirectory($this->configuration, $dir);
$path = $dir . '/' . $className . '.php';
file_put_contents($path, $code);
return $path;
}
private function getTemplate(): string
{
if ($this->template === null) {
$this->template = $this->loadCustomTemplate();
if ($this->template === null) {
$this->template = self::MIGRATION_TEMPLATE;
}
}
return $this->template;
}
/** @throws InvalidTemplateSpecified */
private function loadCustomTemplate(): string|null
{
$customTemplate = $this->configuration->getCustomTemplate();
if ($customTemplate === null) {
return null;
}
if (! is_file($customTemplate) || ! is_readable($customTemplate)) {
throw InvalidTemplateSpecified::notFoundOrNotReadable($customTemplate);
}
$content = file_get_contents($customTemplate);
if ($content === false) {
throw InvalidTemplateSpecified::notReadable($customTemplate);
}
if (trim($content) === '') {
throw InvalidTemplateSpecified::empty($customTemplate);
}
return $content;
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Generator;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Metadata\Storage\TableMetadataStorageConfiguration;
use Doctrine\SqlFormatter\NullHighlighter;
use Doctrine\SqlFormatter\SqlFormatter;
use function array_unshift;
use function count;
use function get_class;
use function implode;
use function preg_replace;
use function sprintf;
use function str_repeat;
use function stripos;
use function strlen;
use function var_export;
/**
* The SqlGenerator class is responsible for generating the body of the up() and down() methods for a migration
* from an array of SQL queries.
*
* @internal
*/
class SqlGenerator
{
private SqlFormatter|null $formatter = null;
public function __construct(
private readonly Configuration $configuration,
private readonly AbstractPlatform $platform,
) {
}
/** @param string[] $sql */
public function generate(
array $sql,
bool $formatted = false,
bool|null $nowdocOutput = null,
int $lineLength = 120,
bool $checkDbPlatform = true,
): string {
$code = [];
$storageConfiguration = $this->configuration->getMetadataStorageConfiguration();
$maxLength = $lineLength - 18 - 8; // max - php code length - indentation
foreach ($sql as $query) {
if (
$storageConfiguration instanceof TableMetadataStorageConfiguration
&& stripos($query, $storageConfiguration->getTableName()) !== false
) {
continue;
}
if ($formatted && strlen($query) > $maxLength) {
$query = $this->formatQuery($query);
}
if ($nowdocOutput === true || ($nowdocOutput !== false && $formatted && strlen($query) > $maxLength )) {
$code[] = sprintf(
"\$this->addSql(<<<'SQL'\n%s\nSQL);",
preg_replace('/^/m', str_repeat(' ', 4), $query),
);
} else {
$code[] = sprintf('$this->addSql(%s);', var_export($query, true));
}
}
if (count($code) !== 0 && $checkDbPlatform && $this->configuration->isDatabasePlatformChecked()) {
$currentPlatform = '\\' . get_class($this->platform);
array_unshift(
$code,
sprintf(
<<<'PHP'
$this->abortIf(
!$this->connection->getDatabasePlatform() instanceof %s,
"Migration can only be executed safely on '%s'."
);
PHP
,
$currentPlatform,
$currentPlatform,
),
'',
);
}
return implode("\n", $code);
}
private function formatQuery(string $query): string
{
$this->formatter ??= new SqlFormatter(new NullHighlighter());
return $this->formatter->format($query);
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Types\Type;
use function array_map;
use function implode;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function sprintf;
/**
* The InlineParameterFormatter class is responsible for formatting SQL query parameters to a string
* for display output.
*
* @internal
*/
final class InlineParameterFormatter implements ParameterFormatter
{
public function __construct(private readonly Connection $connection)
{
}
/**
* @param mixed[] $params
* @param mixed[] $types
*/
public function formatParameters(array $params, array $types): string
{
if ($params === []) {
return '';
}
$formattedParameters = [];
foreach ($params as $key => $value) {
$type = $types[$key] ?? 'string';
$formattedParameter = '[' . $this->formatParameter($value, $type) . ']';
$formattedParameters[] = is_string($key)
? sprintf(':%s => %s', $key, $formattedParameter)
: $formattedParameter;
}
return sprintf('with parameters (%s)', implode(', ', $formattedParameters));
}
private function formatParameter(mixed $value, mixed $type): string|int|bool|float|null
{
if (is_string($type) && Type::hasType($type)) {
return Type::getType($type)->convertToDatabaseValue(
$value,
$this->connection->getDatabasePlatform(),
);
}
return $this->parameterToString($value);
}
/** @param int[]|bool[]|string[]|float[]|array|int|string|float|bool $value */
private function parameterToString(array|int|string|float|bool $value): string
{
if (is_array($value)) {
return implode(', ', array_map($this->parameterToString(...), $value));
}
if (is_int($value) || is_string($value) || is_float($value)) {
return (string) $value;
}
if (is_bool($value)) {
return $value === true ? 'true' : 'false';
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use Doctrine\Migrations\AbstractMigration;
use Doctrine\Migrations\Version\Version;
/**
* Available migrations may or may not be already executed
* The migration might be already executed or not.
*/
final class AvailableMigration
{
public function __construct(
private readonly Version $version,
private readonly AbstractMigration $migration,
) {
}
public function getVersion(): Version
{
return $this->version;
}
public function getMigration(): AbstractMigration
{
return $this->migration;
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use Countable;
use Doctrine\Migrations\Exception\MigrationNotAvailable;
use Doctrine\Migrations\Exception\NoMigrationsFoundWithCriteria;
use Doctrine\Migrations\Version\Version;
use function array_filter;
use function array_values;
use function count;
/**
* Represents a sorted list of migrations that may or maybe not be already executed.
*/
final class AvailableMigrationsList implements Countable
{
/** @var AvailableMigration[] */
private array $items = [];
/** @param AvailableMigration[] $items */
public function __construct(array $items)
{
$this->items = array_values($items);
}
/** @return AvailableMigration[] */
public function getItems(): array
{
return $this->items;
}
public function getFirst(int $offset = 0): AvailableMigration
{
if (! isset($this->items[$offset])) {
throw NoMigrationsFoundWithCriteria::new('first' . ($offset > 0 ? '+' . $offset : ''));
}
return $this->items[$offset];
}
public function getLast(int $offset = 0): AvailableMigration
{
$offset = count($this->items) - 1 - (-1 * $offset);
if (! isset($this->items[$offset])) {
throw NoMigrationsFoundWithCriteria::new('last' . ($offset > 0 ? '+' . $offset : ''));
}
return $this->items[$offset];
}
public function count(): int
{
return count($this->items);
}
public function hasMigration(Version $version): bool
{
foreach ($this->items as $migration) {
if ($migration->getVersion()->equals($version)) {
return true;
}
}
return false;
}
public function getMigration(Version $version): AvailableMigration
{
foreach ($this->items as $migration) {
if ($migration->getVersion()->equals($version)) {
return $migration;
}
}
throw MigrationNotAvailable::forVersion($version);
}
public function newSubset(ExecutedMigrationsList $executedMigrations): self
{
return new self(array_filter($this->getItems(), static fn (AvailableMigration $migration): bool => ! $executedMigrations->hasMigration($migration->getVersion())));
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use Countable;
use Doctrine\Migrations\Exception\MigrationNotAvailable;
use Doctrine\Migrations\Version\Version;
use function array_values;
use function count;
/**
* Represents a non sorted list of migrations that may or may not be already executed.
*/
final class AvailableMigrationsSet implements Countable
{
/** @var AvailableMigration[] */
private array $items = [];
/** @param AvailableMigration[] $items */
public function __construct(array $items)
{
$this->items = array_values($items);
}
/** @return AvailableMigration[] */
public function getItems(): array
{
return $this->items;
}
public function count(): int
{
return count($this->items);
}
public function hasMigration(Version $version): bool
{
foreach ($this->items as $migration) {
if ($migration->getVersion()->equals($version)) {
return true;
}
}
return false;
}
public function getMigration(Version $version): AvailableMigration
{
foreach ($this->items as $migration) {
if ($migration->getVersion()->equals($version)) {
return $migration;
}
}
throw MigrationNotAvailable::forVersion($version);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use DateTimeImmutable;
use Doctrine\Migrations\Version\Version;
/**
* Represents an already executed migration.
* The migration might be not available anymore.
*/
final class ExecutedMigration
{
public function __construct(
private readonly Version $version,
private readonly DateTimeImmutable|null $executedAt = null,
public float|null $executionTime = null,
) {
}
public function getExecutionTime(): float|null
{
return $this->executionTime;
}
public function getExecutedAt(): DateTimeImmutable|null
{
return $this->executedAt;
}
public function getVersion(): Version
{
return $this->version;
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use Countable;
use Doctrine\Migrations\Exception\MigrationNotExecuted;
use Doctrine\Migrations\Exception\NoMigrationsFoundWithCriteria;
use Doctrine\Migrations\Version\Version;
use function array_filter;
use function array_values;
use function count;
/**
* Represents a sorted list of executed migrations.
* The migrations in this set might be not available anymore.
*/
final class ExecutedMigrationsList implements Countable
{
/** @var ExecutedMigration[] */
private array $items = [];
/** @param ExecutedMigration[] $items */
public function __construct(array $items)
{
$this->items = array_values($items);
}
/** @return ExecutedMigration[] */
public function getItems(): array
{
return $this->items;
}
public function getFirst(int $offset = 0): ExecutedMigration
{
if (! isset($this->items[$offset])) {
throw NoMigrationsFoundWithCriteria::new('first' . ($offset > 0 ? '+' . $offset : ''));
}
return $this->items[$offset];
}
public function getLast(int $offset = 0): ExecutedMigration
{
$offset = count($this->items) - 1 - (-1 * $offset);
if (! isset($this->items[$offset])) {
throw NoMigrationsFoundWithCriteria::new('last' . ($offset > 0 ? '+' . $offset : ''));
}
return $this->items[$offset];
}
public function count(): int
{
return count($this->items);
}
public function hasMigration(Version $version): bool
{
foreach ($this->items as $migration) {
if ($migration->getVersion()->equals($version)) {
return true;
}
}
return false;
}
public function getMigration(Version $version): ExecutedMigration
{
foreach ($this->items as $migration) {
if ($migration->getVersion()->equals($version)) {
return $migration;
}
}
throw MigrationNotExecuted::new((string) $version);
}
public function unavailableSubset(AvailableMigrationsList $availableMigrations): self
{
return new self(array_filter($this->getItems(), static fn (ExecutedMigration $migration): bool => ! $availableMigrations->hasMigration($migration->getVersion())));
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use Doctrine\Migrations\AbstractMigration;
use Doctrine\Migrations\Exception\PlanAlreadyExecuted;
use Doctrine\Migrations\Version\ExecutionResult;
use Doctrine\Migrations\Version\Version;
/**
* Represents an available migration to be executed in a specific direction.
*/
final class MigrationPlan
{
public ExecutionResult|null $result = null;
public function __construct(
private readonly Version $version,
private readonly AbstractMigration $migration,
private readonly string $direction,
) {
}
public function getVersion(): Version
{
return $this->version;
}
public function getResult(): ExecutionResult|null
{
return $this->result;
}
public function markAsExecuted(ExecutionResult $result): void
{
if ($this->result !== null) {
throw PlanAlreadyExecuted::new();
}
$this->result = $result;
}
public function getMigration(): AbstractMigration
{
return $this->migration;
}
public function getDirection(): string
{
return $this->direction;
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use Countable;
use Doctrine\Migrations\Exception\NoMigrationsFoundWithCriteria;
use function count;
use function end;
use function reset;
/**
* Represents a sorted list of MigrationPlan instances to execute.
*/
final class MigrationPlanList implements Countable
{
/** @param MigrationPlan[] $items */
public function __construct(
private array $items,
private readonly string $direction,
) {
}
public function count(): int
{
return count($this->items);
}
/** @return MigrationPlan[] */
public function getItems(): array
{
return $this->items;
}
public function getDirection(): string
{
return $this->direction;
}
public function getFirst(): MigrationPlan
{
if (count($this->items) === 0) {
throw NoMigrationsFoundWithCriteria::new('first');
}
return reset($this->items);
}
public function getLast(): MigrationPlan
{
if (count($this->items) === 0) {
throw NoMigrationsFoundWithCriteria::new('last');
}
return end($this->items);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata\Storage;
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
use Doctrine\Migrations\Query\Query;
use Doctrine\Migrations\Version\ExecutionResult;
/** @method iterable<Query> getSql(ExecutionResult $result); */
interface MetadataStorage
{
public function ensureInitialized(): void;
public function getExecutedMigrations(): ExecutedMigrationsList;
public function complete(ExecutionResult $result): void;
public function reset(): void;
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata\Storage;
interface MetadataStorageConfiguration
{
}

View File

@@ -0,0 +1,312 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata\Storage;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\ComparatorConfig;
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\Exception\MetadataStorageError;
use Doctrine\Migrations\Metadata\AvailableMigration;
use Doctrine\Migrations\Metadata\ExecutedMigration;
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
use Doctrine\Migrations\MigrationsRepository;
use Doctrine\Migrations\Query\Query;
use Doctrine\Migrations\Version\Comparator as MigrationsComparator;
use Doctrine\Migrations\Version\Direction;
use Doctrine\Migrations\Version\ExecutionResult;
use Doctrine\Migrations\Version\Version;
use InvalidArgumentException;
use function array_change_key_case;
use function class_exists;
use function floatval;
use function method_exists;
use function round;
use function sprintf;
use function strlen;
use function strpos;
use function strtolower;
use function uasort;
use const CASE_LOWER;
final class TableMetadataStorage implements MetadataStorage
{
private bool $isInitialized = false;
private bool $schemaUpToDate = false;
/** @var AbstractSchemaManager<AbstractPlatform> */
private readonly AbstractSchemaManager $schemaManager;
private readonly AbstractPlatform $platform;
private readonly TableMetadataStorageConfiguration $configuration;
public function __construct(
private readonly Connection $connection,
private readonly MigrationsComparator $comparator,
MetadataStorageConfiguration|null $configuration = null,
private readonly MigrationsRepository|null $migrationRepository = null,
) {
$this->schemaManager = $connection->createSchemaManager();
$this->platform = $connection->getDatabasePlatform();
if ($configuration !== null && ! ($configuration instanceof TableMetadataStorageConfiguration)) {
throw new InvalidArgumentException(sprintf(
'%s accepts only %s as configuration',
self::class,
TableMetadataStorageConfiguration::class,
));
}
$this->configuration = $configuration ?? new TableMetadataStorageConfiguration();
}
public function getExecutedMigrations(): ExecutedMigrationsList
{
if (! $this->isInitialized()) {
return new ExecutedMigrationsList([]);
}
$this->checkInitialization();
$rows = $this->connection->fetchAllAssociative(sprintf('SELECT * FROM %s', $this->configuration->getTableName()));
$migrations = [];
foreach ($rows as $row) {
$row = array_change_key_case($row, CASE_LOWER);
$version = new Version($row[strtolower($this->configuration->getVersionColumnName())]);
$executedAt = $row[strtolower($this->configuration->getExecutedAtColumnName())] ?? '';
$executedAt = $executedAt !== ''
? DateTimeImmutable::createFromFormat($this->platform->getDateTimeFormatString(), $executedAt)
: null;
$executionTime = isset($row[strtolower($this->configuration->getExecutionTimeColumnName())])
? floatval($row[strtolower($this->configuration->getExecutionTimeColumnName())] / 1000)
: null;
$migration = new ExecutedMigration(
$version,
$executedAt instanceof DateTimeImmutable ? $executedAt : null,
$executionTime,
);
$migrations[(string) $version] = $migration;
}
uasort($migrations, fn (ExecutedMigration $a, ExecutedMigration $b): int => $this->comparator->compare($a->getVersion(), $b->getVersion()));
return new ExecutedMigrationsList($migrations);
}
public function reset(): void
{
$this->checkInitialization();
$this->connection->executeStatement(
sprintf(
'DELETE FROM %s WHERE 1 = 1',
$this->configuration->getTableName(),
),
);
}
public function complete(ExecutionResult $result): void
{
$this->checkInitialization();
if ($result->getDirection() === Direction::DOWN) {
$this->connection->delete($this->configuration->getTableName(), [
$this->configuration->getVersionColumnName() => (string) $result->getVersion(),
]);
} else {
$this->connection->insert($this->configuration->getTableName(), [
$this->configuration->getVersionColumnName() => (string) $result->getVersion(),
$this->configuration->getExecutedAtColumnName() => $result->getExecutedAt(),
$this->configuration->getExecutionTimeColumnName() => $result->getTime() === null ? null : (int) round($result->getTime() * 1000),
], [
Types::STRING,
Types::DATETIME_IMMUTABLE,
Types::INTEGER,
]);
}
}
/** @return iterable<Query> */
public function getSql(ExecutionResult $result): iterable
{
yield new Query('-- Version ' . (string) $result->getVersion() . ' update table metadata');
if ($result->getDirection() === Direction::DOWN) {
yield new Query(sprintf(
'DELETE FROM %s WHERE %s = %s',
$this->configuration->getTableName(),
$this->configuration->getVersionColumnName(),
$this->connection->quote((string) $result->getVersion()),
));
return;
}
yield new Query(sprintf(
'INSERT INTO %s (%s, %s, %s) VALUES (%s, %s, 0)',
$this->configuration->getTableName(),
$this->configuration->getVersionColumnName(),
$this->configuration->getExecutedAtColumnName(),
$this->configuration->getExecutionTimeColumnName(),
$this->connection->quote((string) $result->getVersion()),
$this->connection->quote(($result->getExecutedAt() ?? new DateTimeImmutable())->format('Y-m-d H:i:s')),
));
}
public function ensureInitialized(): void
{
if (! $this->isInitialized()) {
$expectedSchemaChangelog = $this->getExpectedTable();
$this->schemaManager->createTable($expectedSchemaChangelog);
$this->schemaUpToDate = true;
$this->isInitialized = true;
return;
}
$this->isInitialized = true;
$expectedSchemaChangelog = $this->getExpectedTable();
$diff = $this->needsUpdate($expectedSchemaChangelog);
if ($diff === null) {
$this->schemaUpToDate = true;
return;
}
$this->schemaUpToDate = true;
$this->schemaManager->alterTable($diff);
$this->updateMigratedVersionsFromV1orV2toV3();
}
private function needsUpdate(Table $expectedTable): TableDiff|null
{
if ($this->schemaUpToDate) {
return null;
}
if (class_exists(ComparatorConfig::class)) {
$comparator = $this->schemaManager->createComparator((new ComparatorConfig())->withReportModifiedIndexes(false));
} else {
$comparator = $this->schemaManager->createComparator();
}
/** @phpstan-ignore function.alreadyNarrowedType */
if (method_exists($this->schemaManager, 'introspectTableByUnquotedName')) {
$currentTable = $this->schemaManager->introspectTableByUnquotedName($this->configuration->getTableName());
} else {
/** @phpstan-ignore method.deprecated */
$currentTable = $this->schemaManager->introspectTable($this->configuration->getTableName());
}
$diff = $comparator->compareTables($currentTable, $expectedTable);
return $diff->isEmpty() ? null : $diff;
}
private function isInitialized(): bool
{
if ($this->isInitialized) {
return $this->isInitialized;
}
if ($this->connection instanceof PrimaryReadReplicaConnection) {
$this->connection->ensureConnectedToPrimary();
}
return $this->schemaManager->tablesExist([$this->configuration->getTableName()]);
}
private function checkInitialization(): void
{
if (! $this->isInitialized()) {
throw MetadataStorageError::notInitialized();
}
$expectedTable = $this->getExpectedTable();
if ($this->needsUpdate($expectedTable) !== null) {
throw MetadataStorageError::notUpToDate();
}
}
private function getExpectedTable(): Table
{
$schemaChangelog = new Table($this->configuration->getTableName());
$schemaChangelog->addColumn(
$this->configuration->getVersionColumnName(),
'string',
['notnull' => true, 'length' => $this->configuration->getVersionColumnLength()],
);
$schemaChangelog->addColumn($this->configuration->getExecutedAtColumnName(), 'datetime', ['notnull' => false]);
$schemaChangelog->addColumn($this->configuration->getExecutionTimeColumnName(), 'integer', ['notnull' => false]);
if (class_exists(PrimaryKeyConstraint::class)) {
$constraint = PrimaryKeyConstraint::editor()
->setColumnNames(UnqualifiedName::unquoted($this->configuration->getVersionColumnName()))
->create();
$schemaChangelog->addPrimaryKeyConstraint($constraint);
} else {
$schemaChangelog->setPrimaryKey([$this->configuration->getVersionColumnName()]);
}
return $schemaChangelog;
}
private function updateMigratedVersionsFromV1orV2toV3(): void
{
if ($this->migrationRepository === null) {
return;
}
$availableMigrations = $this->migrationRepository->getMigrations()->getItems();
$executedMigrations = $this->getExecutedMigrations()->getItems();
foreach ($availableMigrations as $availableMigration) {
foreach ($executedMigrations as $k => $executedMigration) {
if ($this->isAlreadyV3Format($availableMigration, $executedMigration)) {
continue;
}
$this->connection->update(
$this->configuration->getTableName(),
[
$this->configuration->getVersionColumnName() => (string) $availableMigration->getVersion(),
],
[
$this->configuration->getVersionColumnName() => (string) $executedMigration->getVersion(),
],
);
unset($executedMigrations[$k]);
}
}
}
private function isAlreadyV3Format(AvailableMigration $availableMigration, ExecutedMigration $executedMigration): bool
{
return (string) $availableMigration->getVersion() === (string) $executedMigration->getVersion()
|| strpos(
(string) $availableMigration->getVersion(),
(string) $executedMigration->getVersion(),
) !== strlen((string) $availableMigration->getVersion()) -
strlen((string) $executedMigration->getVersion());
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata\Storage;
final class TableMetadataStorageConfiguration implements MetadataStorageConfiguration
{
/** @var non-empty-string */
private string $tableName = 'doctrine_migration_versions';
/** @var non-empty-string */
private string $versionColumnName = 'version';
private int $versionColumnLength = 191;
private string $executedAtColumnName = 'executed_at';
private string $executionTimeColumnName = 'execution_time';
/** @return non-empty-string */
public function getTableName(): string
{
return $this->tableName;
}
/** @param non-empty-string $tableName */
public function setTableName(string $tableName): void
{
$this->tableName = $tableName;
}
/** @return non-empty-string */
public function getVersionColumnName(): string
{
return $this->versionColumnName;
}
/** @param non-empty-string $versionColumnName */
public function setVersionColumnName(string $versionColumnName): void
{
$this->versionColumnName = $versionColumnName;
}
public function getVersionColumnLength(): int
{
return $this->versionColumnLength;
}
public function setVersionColumnLength(int $versionColumnLength): void
{
$this->versionColumnLength = $versionColumnLength;
}
public function getExecutedAtColumnName(): string
{
return $this->executedAtColumnName;
}
public function setExecutedAtColumnName(string $executedAtColumnName): void
{
$this->executedAtColumnName = $executedAtColumnName;
}
public function getExecutionTimeColumnName(): string
{
return $this->executionTimeColumnName;
}
public function setExecutionTimeColumnName(string $executionTimeColumnName): void
{
$this->executionTimeColumnName = $executionTimeColumnName;
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations;
use Doctrine\Migrations\Metadata\AvailableMigration;
use Doctrine\Migrations\Metadata\AvailableMigrationsSet;
use Doctrine\Migrations\Version\Version;
interface MigrationsRepository
{
public function hasMigration(string $version): bool;
public function getMigration(Version $version): AvailableMigration;
public function getMigrations(): AvailableMigrationsSet;
}

Some files were not shown because too many files have changed in this diff Show More