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,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();
}
}