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,19 @@
Copyright (c) 2006-2018 Doctrine Project
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,11 @@
# Doctrine Migrations
[![Build Status](https://github.com/doctrine/migrations/workflows/Continuous%20Integration/badge.svg)](https://github.com/doctrine/migrations/actions)
[![Code Coverage](https://codecov.io/gh/doctrine/migrations/branch/3.1.x/graph/badge.svg)](https://codecov.io/gh/doctrine/migrations/branch/3.1.x)
[![Packagist Downloads](https://img.shields.io/packagist/dm/doctrine/migrations)](https://packagist.org/packages/doctrine/migrations)
[![Packagist Version](https://img.shields.io/packagist/v/doctrine/migrations)](https://packagist.org/packages/doctrine/migrations)
[![GitHub license](https://img.shields.io/github/license/doctrine/migrations)](LICENSE)
## Documentation
All available documentation can be found [here](https://www.doctrine-project.org/projects/migrations.html).

View File

@@ -0,0 +1,302 @@
# Upgrade to 3.6
## Console
- The `--all-or-nothing` option for `migrations:migrate` does not accept a value anymore, and passing it a
value will generate a deprecation. Specifying `--all-or-nothing` will wrap all the migrations to be
executed into a single transaction, regardless of the specified configuration.
# Upgrade to 3.1
- The "version" is the FQCN of the migration class (existing entries in the migrations table will be automatically updated).
- `MigrationsEventArgs` and `MigrationsVersionEventArgs` expose different API,
please refer to the [Code BC breaks](#code-bc-breaks) section.
## Console
- Console output changed. The commands use a different output style. If you were relying on specific output,
please update your scripts.
Console output is not covered by the BC promise, so please try not to rely on specific a output.
Different levels of verbosity are available now (`-v`, `-vv` and `-vvv` ).
- The `--show-versions` option from `migrations:status` command has been removed,
use `migrations:list` instead.
- The `--write-sql` option for `migrations:migrate` and `migrations:execute` does not imply dry-run anymore,
use the `--dry-run` parameter instead.
- The `--db` option has been renamed to `--conn`.
## Migrations table
- The migrations table now has a new column named `execution_time`.
- Running the `migrations:migrate` or `migrations:execute` command will automatically upgrade the migration
table structure; a dedicated `migrations:sync-metadata-storage` command is available to sync manually the migrations table.
## Migration template
- The `<version>` placeholder has been replaced by the `<className>` placeholder.
## Configuration files
*migrations.php Before*
```php
<?php
return [
'name' => 'My Project Migrations',
'migrations_namespace' => 'MyProject\Migrations',
'table_name' => 'doctrine_migration_versions',
'column_name' => 'version',
'column_length' => 14,
'executed_at_column_name' => 'executed_at',
'migrations_directory' => '/data/doctrine/migrations-docs-example/lib/MyProject/Migrations',
'all_or_nothing' => true,
'check_database_platform' => true,
];
```
*migrations.php After*
```php
<?php
return [
'table_storage' => [
'table_name' => 'doctrine_migration_versions',
'version_column_name' => 'version',
'version_column_length' => 191,
'executed_at_column_name' => 'executed_at',
'execution_time_column_name' => 'execution_time',
],
'migrations_paths' => [
'MyProject\Migrations' => '/data/doctrine/migrations/lib/MyProject/Migrations',
'MyProject\Component\Migrations' => './Component/MyProject/Migrations',
],
'all_or_nothing' => true,
'check_database_platform' => true,
];
```
Files in XML, YAML or JSON also changed in a similar way. Please refer to the official documentation for more details.
Note: the `name` property has been removed.
Note: the option in `table_storage` needs to be updated only if you have changed the metadata table settings
by using v2 options such as `table_name`, `column_name`, `column_length` or `executed_at_column_name`. If you did not change
those settings, it is recommended to not provide the options and let doctrine figure out the best settings.
## Code BC breaks
Most of the code is protected by the `@internal` declaration and in a very rare cases you might have dealt with the
internals of this library.
The most important BC breaks are in the `Doctrine\Migrations\Configuration\Configuration` class and in the helper
system that now has been replaced by the `Doctrine\Migrations\DependencyFactory` functionalities.
Here is a list of the most important changes:
- Namespace `Doctrine\Migrations\Configuration\Configuration`
- CHANGED: Class `Doctrine\Migrations\Configuration\Configuration` became final
- REMOVED: Constant `Doctrine\Migrations\Configuration\Configuration::VERSION_FORMAT` was removed, there is not more limitation on the version format
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#__construct()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#setName()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getName()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getConnection()` was removed,
use `Doctrine\Migrations\DependencyFactory#getConnection()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#setMigrationsTableName()` was removed,
use `Doctrine\Migrations\Configuration\Configuration#setMetadataStorageConfiguration` with an instance of `Doctrine\Migrations\Metadata\Storage\MetadataStorageConfiguration`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getMigrationsTableName()` was removed,
use `Doctrine\Migrations\Metadata\Storage\MetadataStorageConfiguration#getMetadataStorageConfiguration`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#setMigrationsColumnName()` was removed,
use `Doctrine\Migrations\Configuration\Configuration#setMetadataStorageConfiguration` with an instance of `Doctrine\Migrations\Metadata\Storage\MetadataStorageConfiguration`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getMigrationsColumnName()` was removed,
use `Doctrine\Migrations\Metadata\Storage\MetadataStorageConfiguration#getMetadataStorageConfiguration`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getQuotedMigrationsColumnName()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#setMigrationsColumnLength()` was removed,
use `Doctrine\Migrations\Configuration\Configuration#setMetadataStorageConfiguration` with an instance of `Doctrine\Migrations\Metadata\Storage\MetadataStorageConfiguration`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getMigrationsColumnLength()` was removed,
use `Doctrine\Migrations\Metadata\Storage\MetadataStorageConfiguration#getMetadataStorageConfiguration`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#setMigrationsExecutedAtColumnName()` was removed,
use `Doctrine\Migrations\Configuration\Configuration#setMetadataStorageConfiguration` with an instance of `Doctrine\Migrations\Metadata\Storage\MetadataStorageConfiguration`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getMigrationsExecutedAtColumnName()` was removed,
use `Doctrine\Migrations\Metadata\Storage\MetadataStorageConfiguration#getMetadataStorageConfiguration`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getQuotedMigrationsExecutedAtColumnName()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#setMigrationsDirectory()` was removed,
use `Doctrine\Migrations\Configuration\Configuration#addMigrationsDirectory()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getMigrationsDirectory()` was removed,
use `Doctrine\Migrations\Configuration\Configuration#getMigrationDirectories()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#setMigrationsNamespace()` was removed,
use `Doctrine\Migrations\Configuration\Configuration#addMigrationsDirectory()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getMigrationsNamespace()` was removed,
use `Doctrine\Migrations\Configuration\Configuration#getMigrationDirectories()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#setMigrationsFinder()` was removed,
use the dependency factory instead
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getMigrationsFinder()` was removed,
use the dependency factory instead
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#hasVersionMigrated()` was removed,
use the dependency factory instead
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getVersionData()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#resolveVersionAlias()` was removed,
use `Doctrine\Migrations\Version\AliasResolver#resolveVersionAlias()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#isMigrationTableCreated()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#createMigrationTable()` was removed,
use `Doctrine\Migrations\Metadata\Storage\MetadataStorage#ensureInitialized()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getDateTime()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#generateVersionNumber()` was removed,
use `Doctrine\Migrations\Generator\ClassNameGenerator#generateClassName()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#connect()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#dispatchMigrationEvent()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#dispatchVersionEvent()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#dispatchEvent()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getNumberOfExecutedMigrations()` was removed,
use `Doctrine\Migrations\DependencyFactory#getMetadataStorage()->getExecutedMigrations()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getNumberOfAvailableMigrations()` was removed,
use `Doctrine\Migrations\DependencyFactory#getMigrationRepository()->getMigrations()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getLatestVersion()` was removed,
use `Doctrine\Migrations\Version\AliasResolver#resolveVersionAlias()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getMigratedVersions()` was removed,
use `Doctrine\Migrations\DependencyFactory#getMetadataStorage()->getExecutedMigrations()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getAvailableVersions()` was removed
use `Doctrine\Migrations\DependencyFactory#getMigrationRepository()->getMigrations()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getCurrentVersion()` was removed,
use `Doctrine\Migrations\Version\AliasResolver#resolveVersionAlias()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#registerMigrationsFromDirectory()` was removed,
use `Doctrine\Migrations\Configuration\Configuration#addMigrationsDirectory()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#registerMigration()` was removed,
use `Doctrine\Migrations\Configuration\Configuration#addMigrationClass()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#registerMigrations()` was removed
use `Doctrine\Migrations\Configuration\Configuration#addMigrationClass()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getMigrations()` was removed,
use `Doctrine\Migrations\DependencyFactory#getMigrationRepository()->getMigrations()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getVersion()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getMigrationsToExecute()` was removed,
use `Doctrine\Migrations\Version\MigrationPlanCalculator#getPlanUntilVersion()` to create a migration plan
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getPrevVersion()` was removed,
use `Doctrine\Migrations\Version\AliasResolver#resolveVersionAlias()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getNextVersion()` was removed,
use `Doctrine\Migrations\Version\AliasResolver#resolveVersionAlias()`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getRelativeVersion()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getDeltaVersion()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#setOutputWriter()` was removed,
set the `Psr\Log\LoggerInterface` service in `Doctrine\Migrations\DependencyFactory`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getOutputWriter()` was removed,
get the `Psr\Log\LoggerInterface` service from `Doctrine\Migrations\DependencyFactory`
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getQueryWriter()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#getDependencyFactory()` was removed
- REMOVED: Method `Doctrine\Migrations\Configuration\Configuration#validate()` was removed
- Namespace `Doctrine\Migrations\Configuration\Connection\Loader\Exception`
- REMOVED: Class `Doctrine\Migrations\Configuration\Connection\Loader\Exception\LoaderException` has been deleted
- REMOVED: Class `Doctrine\Migrations\Configuration\Connection\Loader\Exception\InvalidConfiguration` has been deleted
- Namespace `Doctrine\Migrations\Configuration\Exception`
- REMOVED: Class `Doctrine\Migrations\Configuration\Exception\ParameterIncompatibleWithFinder` has been deleted
- REMOVED: Class `Doctrine\Migrations\Configuration\Exception\InvalidConfigurationKey` has been deleted
- REMOVED: Class `Doctrine\Migrations\Configuration\Exception\MigrationsNamespaceRequired` has been deleted
- REMOVED: Class `Doctrine\Migrations\Configuration\Exception\XmlNotValid` has been deleted
- REMOVED: Class `Doctrine\Migrations\Configuration\Exception\YamlNotAvailable` has been deleted
- REMOVED: Class `Doctrine\Migrations\Configuration\Exception\FileAlreadyLoaded` has been deleted
- REMOVED: Class `Doctrine\Migrations\Configuration\Exception\JsonNotValid` has been deleted
- REMOVED: Class `Doctrine\Migrations\Configuration\Exception\YamlNotValid` has been deleted
- CHANGED: The number of required arguments for `Doctrine\Migrations\Configuration\Exception\FileNotFound::new()` increased from 0 to 1
- Namespace `Doctrine\Migrations\Event\MigrationsEventArgs`
- CHANGED: Class `Doctrine\Migrations\Event\MigrationsEventArgs` became final
- REMOVED: Method `Doctrine\Migrations\Event\MigrationsEventArgs#getConfiguration()` was removed
- REMOVED: Method `Doctrine\Migrations\Event\MigrationsEventArgs#getDirection()` was removed,
use `Doctrine\Migrations\Event\MigrationsEventArgs#getPlan()`
- REMOVED: Method `Doctrine\Migrations\Event\MigrationsEventArgs#isDryRun()` was removed,
use `Doctrine\Migrations\Event\MigrationsEventArgs#getMigratorConfiguration()`
- CHANGED: `Doctrine\Migrations\Event\MigrationsEventArgs#__construct()` arguments have been updated
- Namespace `Doctrine\Migrations\Event\MigrationsVersionEventArgs`
- CHANGED: Class `Doctrine\Migrations\Event\MigrationsVersionEventArgs` became final
- REMOVED: Method `Doctrine\Migrations\Event\MigrationsVersionEventArgs#getVersion()` was removed
use `Doctrine\Migrations\Event\MigrationsEventArgs#getPlan()`
- Namespace `Doctrine\Migrations\Finder`
- REMOVED: These ancestors of `Doctrine\Migrations\Finder\RecursiveRegexFinder` have been removed: ["Doctrine\\Migrations\\Finder\\MigrationDeepFinder"]
- REMOVED: Class `Doctrine\Migrations\Finder\MigrationDeepFinder` has been deleted
- Namespace `Doctrine\Migrations\Tools\Console\Command`
- CHANGED: All non abstract classes in `Doctrine\Migrations\Tools\Console\Command\*` became final
- REMOVED: Class `Doctrine\Migrations\Tools\Console\Command\AbstractCommand` has been renamed into `Doctrine\Migrations\Tools\Console\Command\DoctrineCommand` and has been marked as internal
- CHANGED: Method `Doctrine\Migrations\Tools\Console\Command\*Command#__construct()` changed signature into `(?Doctrine\Migrations\DependencyFactory $di, ?string $name)`
- CHANGED: Method `initialize()` of Class `Doctrine\Migrations\Tools\Console\Command\AbstractCommand` visibility reduced from `public` to `protected`
- CHANGED: Method `execute()` of Class `Doctrine\Migrations\Tools\Console\Command\*Command` visibility reduced from `public` to `protected`
- REMOVED: Method `Doctrine\Migrations\Tools\Console\Command\DiffCommand#createMigrationDiffGenerator()` was removed
- Namespace `Doctrine\Migrations\Tools\Console\Exception`
- CHANGED: The number of required arguments for `Doctrine\Migrations\Tools\Console\Exception\SchemaDumpRequiresNoMigrations::new()` increased from 0 to 1
- REMOVED: Class `Doctrine\Migrations\Tools\Console\Exception\ConnectionNotSpecified` has been deleted
- Namespace `Migrations\Tools\Console\Helper`
- REMOVED: All classes and namespaces are marked as internal or have been removed,
use `Doctrine\Migrations\DependencyFactory` instead
- Namespace `Doctrine\Migrations\AbstractMigration`
- CHANGED: The method `Doctrine\Migrations\AbstractMigration#__construct()` changed signature into `(Doctrine\DBAL\Connection $conn, PSR\Log\LoggerInterface $logger)`
- CHANGED: The method `Doctrine\Migrations\AbstractMigration#down()` is not abstract anymore, the default implementation will abort the migration process
- REMOVED: Property `Doctrine\Migrations\AbstractMigration#$version` was removed
- Namespace `Doctrine\Migrations\Provider`
- REMOVED: Class `Doctrine\Migrations\Provider\SchemaProviderInterface` has been deleted
- REMOVED: These ancestors of `Doctrine\Migrations\Provider\StubSchemaProvider` have been removed: ["Doctrine\\Migrations\\Provider\\SchemaProviderInterface"]
- Namespace `Doctrine\Migrations\Exception`
- REMOVED: Class `Doctrine\Migrations\Exception\MigrationNotConvertibleToSql` has been deleted
- REMOVED: Class `Doctrine\Migrations\Exception\MigrationsDirectoryRequired` has been deleted
- REMOVED: Class `Doctrine\Migrations\Version\Factory` became the interface `Doctrine\Migrations\Version\MigrationFactory`
- REMOVED: Class `Doctrine\Migrations\OutputWriter` has been deleted,
use `Psr\Log\Loggerinterface`
# Upgrade to 2.0
## BC Break: Moved `Doctrine\DBAL\Migrations` to `Doctrine\Migrations`
Your migration classes that previously used to extend `Doctrine\DBAL\Migrations\AbstractMigration` now need to extend
`Doctrine\Migrations\AbstractMigration` instead. The `Doctrine\DBAL\Migrations\AbstractMigration` class will be
deprecated in the `1.8.0` release to prepare for the BC break.
## BC Break: Removed `Doctrine\DBAL\Migrations\MigrationsVersion`
The `Doctrine\DBAL\Migrations\MigrationsVersion` class is no longer available: please refrain from checking the Migrations version at runtime.
## BC Break: Moved `Doctrine\Migrations\Migration` to `Doctrine\Migrations\Migrator`
To make the name more clear and to differentiate from the `AbstractMigration` class, `Migration` was renamed to `Migrator`.
## BC Break: Moved exception classes from `Doctrine\Migrations\%name%Exception` to `Doctrine\Migrations\Exception\%name%`
doctrine/migrations#636
Follows concept introduced in ORM (doctrine/orm#6743 + doctrine/orm#7210) and naming follows pattern accepted in Doctrine CS.
# Upgrade from 1.0-alpha1 to 1.0.0-alpha3
## AbstractMigration
### Before:
The method `getName()` was defined and it's implementation would change the order in which the migration would be processed.
It would cause discrepancies between the file order in a file browser and the order of execution of the migrations.
### After:
The `getName()` method as been removed | set final and new `getDescription()` method has been added.
The goal of this method is to be able to provide context for the migration.
This context is shown for the last migrated migration when the status command is called.
## --write-sql option from the migrate command
### Before:
The `--write-sql` option would only output sql contained in the migration and would not update the table containing the migrated migrations.
### After:
That option now also output the sql queries necessary to update the table containing the state of the migrations.
If you want to go back to the previous behavior just make a request on the bug tracker as for now the need for it is not very clear.
## MigrationsVersion::VERSION
### Before:
`MigrationsVersion::VERSION` used to be a property.
The returned value was fanciful.
### After:
It is now a a function so that a different value can be automatically send back if it's a modified version that's used.
The returned value is now the git tag.
The tag is in lowercase as the other doctrine projects.

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
namespace Doctrine\Migrations;
require __DIR__ . '/doctrine-migrations.php';

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations;
use Doctrine\Migrations\Tools\Console\ConsoleRunner;
use Phar;
use function extension_loaded;
use function file_exists;
use function fwrite;
use const PHP_EOL;
use const STDERR;
(static function (): void {
$autoloadFiles = [
__DIR__ . '/../vendor/autoload.php',
__DIR__ . '/../../../autoload.php',
];
$autoloaderFound = false;
foreach ($autoloadFiles as $autoloadFile) {
if (! file_exists($autoloadFile)) {
continue;
}
require_once $autoloadFile;
$autoloaderFound = true;
}
if (! $autoloaderFound) {
if (extension_loaded('phar') && Phar::running() !== '') {
fwrite(STDERR, 'The PHAR was built without dependencies!' . PHP_EOL);
exit(1);
}
fwrite(STDERR, 'vendor/autoload.php could not be found. Did you run `composer install`?' . PHP_EOL);
exit(1);
}
$dependencyFactory = ConsoleRunner::findDependencyFactory();
ConsoleRunner::run([], $dependencyFactory);
})();

View File

@@ -0,0 +1,84 @@
{
"name": "doctrine/migrations",
"description": "PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily deploying changes to it. It is a very easy to use and a powerful tool.",
"license": "MIT",
"type": "library",
"keywords": [
"database",
"migrations",
"dbal"
],
"authors": [
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Michael Simonson",
"email": "contact@mikesimonson.com"
}
],
"homepage": "https://www.doctrine-project.org/projects/migrations.html",
"require": {
"php": "^8.1",
"composer-runtime-api": "^2",
"doctrine/dbal": "^3.6 || ^4",
"doctrine/deprecations": "^0.5.3 || ^1",
"doctrine/event-manager": "^1.2 || ^2.0",
"psr/log": "^1.1.3 || ^2 || ^3",
"symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"symfony/stopwatch": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"symfony/var-exporter": "^6.2 || ^7.0 || ^8.0"
},
"require-dev": {
"ext-pdo_sqlite": "*",
"doctrine/coding-standard": "^14",
"doctrine/orm": "^2.13 || ^3",
"doctrine/persistence": "^2 || ^3 || ^4",
"doctrine/sql-formatter": "^1.0",
"fig/log-test": "^1",
"phpstan/phpstan": "^2",
"phpstan/phpstan-deprecation-rules": "^2",
"phpstan/phpstan-phpunit": "^2",
"phpstan/phpstan-strict-rules": "^2",
"phpstan/phpstan-symfony": "^2",
"phpunit/phpunit": "^10.3 || ^11.0 || ^12.0",
"symfony/cache": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"symfony/process": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0"
},
"conflict": {
"doctrine/orm": "<2.12 || >=4"
},
"suggest": {
"doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.",
"symfony/yaml": "Allows the use of yaml for migration configuration files."
},
"autoload": {
"psr-4": {
"Doctrine\\Migrations\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Doctrine\\Migrations\\Tests\\": "tests"
}
},
"bin": [
"bin/doctrine-migrations"
],
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
"dealerdirect/phpcodesniffer-composer-installer": true
},
"sort-packages": true
},
"scripts": {
"docs": "composer update --working-dir docs && ./docs/vendor/bin/build-docs.sh @additional_args"
}
}

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;
}
}

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