├── LICENSE ├── README.md ├── UPGRADE.md ├── bin ├── doctrine-migrations └── doctrine-migrations.php ├── composer.json └── src ├── AbstractMigration.php ├── Configuration ├── Configuration.php ├── Connection │ ├── ConfigurationFile.php │ ├── ConnectionLoader.php │ ├── ConnectionRegistryConnection.php │ ├── Exception │ │ ├── ConnectionNotSpecified.php │ │ ├── FileNotFound.php │ │ ├── InvalidConfiguration.php │ │ └── LoaderException.php │ └── ExistingConnection.php ├── EntityManager │ ├── ConfigurationFile.php │ ├── EntityManagerLoader.php │ ├── Exception │ │ ├── FileNotFound.php │ │ ├── InvalidConfiguration.php │ │ └── LoaderException.php │ ├── ExistingEntityManager.php │ └── ManagerRegistryEntityManager.php ├── Exception │ ├── ConfigurationException.php │ ├── FileNotFound.php │ ├── FrozenConfiguration.php │ ├── InvalidLoader.php │ └── UnknownConfigurationValue.php └── Migration │ ├── ConfigurationArray.php │ ├── ConfigurationFile.php │ ├── ConfigurationFileWithFallback.php │ ├── ConfigurationLoader.php │ ├── Exception │ ├── InvalidConfigurationFormat.php │ ├── InvalidConfigurationKey.php │ ├── JsonNotValid.php │ ├── MissingConfigurationFile.php │ ├── XmlNotValid.php │ ├── YamlNotAvailable.php │ └── YamlNotValid.php │ ├── ExistingConfiguration.php │ ├── FormattedFile.php │ ├── JsonFile.php │ ├── PhpFile.php │ ├── XML │ └── configuration.xsd │ ├── XmlFile.php │ └── YamlFile.php ├── DbalMigrator.php ├── DependencyFactory.php ├── Event ├── Listeners │ └── AutoCommitListener.php ├── MigrationsEventArgs.php └── MigrationsVersionEventArgs.php ├── EventDispatcher.php ├── Events.php ├── Exception ├── AbortMigration.php ├── AlreadyAtVersion.php ├── ControlException.php ├── DependencyException.php ├── DuplicateMigrationVersion.php ├── FrozenDependencies.php ├── FrozenMigration.php ├── IrreversibleMigration.php ├── MetadataStorageError.php ├── MigrationClassNotFound.php ├── MigrationConfigurationConflict.php ├── MigrationException.php ├── MigrationNotAvailable.php ├── MigrationNotExecuted.php ├── MissingDependency.php ├── NoMigrationsFoundWithCriteria.php ├── NoMigrationsToExecute.php ├── NoTablesFound.php ├── PlanAlreadyExecuted.php ├── RollupFailed.php ├── SkipMigration.php └── UnknownMigrationVersion.php ├── FileQueryWriter.php ├── FilesystemMigrationsRepository.php ├── Finder ├── Exception │ ├── FinderException.php │ ├── InvalidDirectory.php │ └── NameIsReserved.php ├── Finder.php ├── GlobFinder.php ├── MigrationFinder.php └── RecursiveRegexFinder.php ├── Generator ├── ClassNameGenerator.php ├── ConcatenationFileBuilder.php ├── DiffGenerator.php ├── Exception │ ├── GeneratorException.php │ ├── InvalidTemplateSpecified.php │ └── NoChangesDetected.php ├── FileBuilder.php ├── Generator.php └── SqlGenerator.php ├── InlineParameterFormatter.php ├── Metadata ├── AvailableMigration.php ├── AvailableMigrationsList.php ├── AvailableMigrationsSet.php ├── ExecutedMigration.php ├── ExecutedMigrationsList.php ├── MigrationPlan.php ├── MigrationPlanList.php └── Storage │ ├── MetadataStorage.php │ ├── MetadataStorageConfiguration.php │ ├── TableMetadataStorage.php │ └── TableMetadataStorageConfiguration.php ├── MigrationsRepository.php ├── Migrator.php ├── MigratorConfiguration.php ├── ParameterFormatter.php ├── Provider ├── DBALSchemaDiffProvider.php ├── EmptySchemaProvider.php ├── Exception │ ├── NoMappingFound.php │ └── ProviderException.php ├── LazySchema.php ├── LazySchemaDiffProvider.php ├── OrmSchemaProvider.php ├── SchemaDiffProvider.php ├── SchemaProvider.php └── StubSchemaProvider.php ├── Query ├── Exception │ └── InvalidArguments.php └── Query.php ├── QueryWriter.php ├── Rollup.php ├── SchemaDumper.php ├── Tools ├── BooleanStringFormatter.php ├── BytesFormatter.php ├── Console │ ├── Command │ │ ├── CurrentCommand.php │ │ ├── DiffCommand.php │ │ ├── DoctrineCommand.php │ │ ├── DumpSchemaCommand.php │ │ ├── ExecuteCommand.php │ │ ├── GenerateCommand.php │ │ ├── LatestCommand.php │ │ ├── ListCommand.php │ │ ├── MigrateCommand.php │ │ ├── RollupCommand.php │ │ ├── StatusCommand.php │ │ ├── SyncMetadataCommand.php │ │ ├── UpToDateCommand.php │ │ └── VersionCommand.php │ ├── ConsoleInputMigratorConfigurationFactory.php │ ├── ConsoleRunner.php │ ├── Exception │ │ ├── ConsoleException.php │ │ ├── DependenciesNotSatisfied.php │ │ ├── DirectoryDoesNotExist.php │ │ ├── FileTypeNotSupported.php │ │ ├── InvalidOptionUsage.php │ │ ├── SchemaDumpRequiresNoMigrations.php │ │ ├── VersionAlreadyExists.php │ │ └── VersionDoesNotExist.php │ ├── Helper │ │ ├── ConfigurationHelper.php │ │ ├── MigrationDirectoryHelper.php │ │ └── MigrationStatusInfosHelper.php │ ├── InvalidAllOrNothingConfiguration.php │ └── MigratorConfigurationFactory.php └── TransactionHelper.php └── Version ├── AliasResolver.php ├── AlphabeticalComparator.php ├── Comparator.php ├── CurrentMigrationStatusCalculator.php ├── DbalExecutor.php ├── DbalMigrationFactory.php ├── DefaultAliasResolver.php ├── Direction.php ├── ExecutionResult.php ├── Executor.php ├── MigrationFactory.php ├── MigrationPlanCalculator.php ├── MigrationStatusCalculator.php ├── SortedMigrationPlanCalculator.php ├── State.php └── Version.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2018 Doctrine Project 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doctrine Migrations 2 | 3 | [![Build Status](https://github.com/doctrine/migrations/workflows/Continuous%20Integration/badge.svg)](https://github.com/doctrine/migrations/actions) 4 | [![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) 5 | [![Packagist Downloads](https://img.shields.io/packagist/dm/doctrine/migrations)](https://packagist.org/packages/doctrine/migrations) 6 | [![Packagist Version](https://img.shields.io/packagist/v/doctrine/migrations)](https://packagist.org/packages/doctrine/migrations) 7 | [![GitHub license](https://img.shields.io/github/license/doctrine/migrations)](LICENSE) 8 | 9 | ## Documentation 10 | 11 | All available documentation can be found [here](https://www.doctrine-project.org/projects/migrations.html). 12 | -------------------------------------------------------------------------------- /bin/doctrine-migrations: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | =4" 56 | }, 57 | "suggest": { 58 | "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.", 59 | "symfony/yaml": "Allows the use of yaml for migration configuration files." 60 | }, 61 | "autoload": { 62 | "psr-4": { 63 | "Doctrine\\Migrations\\": "src" 64 | } 65 | }, 66 | "autoload-dev": { 67 | "psr-4": { 68 | "Doctrine\\Migrations\\Tests\\": "tests" 69 | } 70 | }, 71 | "bin": [ 72 | "bin/doctrine-migrations" 73 | ], 74 | "config": { 75 | "allow-plugins": { 76 | "composer/package-versions-deprecated": true, 77 | "dealerdirect/phpcodesniffer-composer-installer": true 78 | }, 79 | "sort-packages": true 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/AbstractMigration.php: -------------------------------------------------------------------------------- 1 | */ 32 | protected $sm; 33 | 34 | /** @var AbstractPlatform */ 35 | protected $platform; 36 | 37 | /** @var Query[] */ 38 | private array $plannedSql = []; 39 | 40 | private bool $frozen = false; 41 | 42 | public function __construct(Connection $connection, private readonly LoggerInterface $logger) 43 | { 44 | $this->connection = $connection; 45 | $this->sm = $this->connection->createSchemaManager(); 46 | $this->platform = $this->connection->getDatabasePlatform(); 47 | } 48 | 49 | /** 50 | * Indicates the transactional mode of this migration. 51 | * 52 | * If this function returns true (default) the migration will be executed 53 | * in one transaction, otherwise non-transactional state will be used to 54 | * execute each of the migration SQLs. 55 | * 56 | * Extending class should override this function to alter the return value. 57 | */ 58 | public function isTransactional(): bool 59 | { 60 | return true; 61 | } 62 | 63 | public function getDescription(): string 64 | { 65 | return ''; 66 | } 67 | 68 | public function warnIf(bool $condition, string $message = 'Unknown Reason'): void 69 | { 70 | if (! $condition) { 71 | return; 72 | } 73 | 74 | $this->logger->warning($message, ['migration' => $this]); 75 | } 76 | 77 | /** @throws AbortMigration */ 78 | public function abortIf(bool $condition, string $message = 'Unknown Reason'): void 79 | { 80 | if ($condition) { 81 | throw new AbortMigration($message); 82 | } 83 | } 84 | 85 | /** @throws SkipMigration */ 86 | public function skipIf(bool $condition, string $message = 'Unknown Reason'): void 87 | { 88 | if ($condition) { 89 | throw new SkipMigration($message); 90 | } 91 | } 92 | 93 | /** @throws MigrationException|DBALException */ 94 | public function preUp(Schema $schema): void 95 | { 96 | } 97 | 98 | /** @throws MigrationException|DBALException */ 99 | public function postUp(Schema $schema): void 100 | { 101 | } 102 | 103 | /** @throws MigrationException|DBALException */ 104 | public function preDown(Schema $schema): void 105 | { 106 | } 107 | 108 | /** @throws MigrationException|DBALException */ 109 | public function postDown(Schema $schema): void 110 | { 111 | } 112 | 113 | /** @throws MigrationException|DBALException */ 114 | abstract public function up(Schema $schema): void; 115 | 116 | /** @throws MigrationException|DBALException */ 117 | public function down(Schema $schema): void 118 | { 119 | $this->abortIf(true, sprintf('No down() migration implemented for "%s"', static::class)); 120 | } 121 | 122 | /** 123 | * @param mixed[] $params 124 | * @param mixed[] $types 125 | */ 126 | protected function addSql( 127 | string $sql, 128 | array $params = [], 129 | array $types = [], 130 | ): void { 131 | if ($this->frozen) { 132 | throw FrozenMigration::new(); 133 | } 134 | 135 | $this->plannedSql[] = new Query($sql, $params, $types); 136 | } 137 | 138 | /** @return Query[] */ 139 | public function getSql(): array 140 | { 141 | return $this->plannedSql; 142 | } 143 | 144 | public function freeze(): void 145 | { 146 | $this->frozen = true; 147 | } 148 | 149 | protected function write(string $message): void 150 | { 151 | $this->logger->notice($message, ['migration' => $this]); 152 | } 153 | 154 | /** @throws IrreversibleMigration */ 155 | protected function throwIrreversibleMigrationException(string|null $message = null): void 156 | { 157 | if ($message === null) { 158 | $message = 'This migration is irreversible and cannot be reverted.'; 159 | } 160 | 161 | throw new IrreversibleMigration($message); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Configuration/Connection/ConfigurationFile.php: -------------------------------------------------------------------------------- 1 | filename)) { 32 | throw FileNotFound::new($this->filename); 33 | } 34 | 35 | $params = include $this->filename; 36 | 37 | if ($params instanceof Connection) { 38 | return $params; 39 | } 40 | 41 | if ($params instanceof ConnectionLoader) { 42 | return $params->getConnection(); 43 | } 44 | 45 | if (is_array($params)) { 46 | return DriverManager::getConnection($params); 47 | } 48 | 49 | throw InvalidConfiguration::invalidArrayConfiguration(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Configuration/Connection/ConnectionLoader.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 21 | $that->defaultConnectionName = $connectionName; 22 | 23 | return $that; 24 | } 25 | 26 | private function __construct() 27 | { 28 | } 29 | 30 | public function getConnection(string|null $name = null): Connection 31 | { 32 | $connection = $this->registry->getConnection($name ?? $this->defaultConnectionName); 33 | if (! $connection instanceof Connection) { 34 | throw InvalidConfiguration::invalidConnectionType($connection); 35 | } 36 | 37 | return $connection; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Configuration/Connection/Exception/ConnectionNotSpecified.php: -------------------------------------------------------------------------------- 1 | connection; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Configuration/EntityManager/ConfigurationFile.php: -------------------------------------------------------------------------------- 1 | filename)) { 36 | throw FileNotFound::new($this->filename); 37 | } 38 | 39 | $params = include $this->filename; 40 | 41 | if ($params instanceof EntityManagerInterface) { 42 | return $params; 43 | } 44 | 45 | if ($params instanceof EntityManagerLoader) { 46 | return $params->getEntityManager(); 47 | } 48 | 49 | throw InvalidConfiguration::invalidArrayConfiguration(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Configuration/EntityManager/EntityManagerLoader.php: -------------------------------------------------------------------------------- 1 | entityManager; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Configuration/EntityManager/ManagerRegistryEntityManager.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 21 | $that->defaultManagerName = $managerName; 22 | 23 | return $that; 24 | } 25 | 26 | private function __construct() 27 | { 28 | } 29 | 30 | public function getEntityManager(string|null $name = null): EntityManagerInterface 31 | { 32 | $managerName = $name ?? $this->defaultManagerName; 33 | 34 | $em = $this->registry->getManager($managerName); 35 | if (! $em instanceof EntityManagerInterface) { 36 | throw InvalidConfiguration::invalidManagerType($em); 37 | } 38 | 39 | return $em; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Configuration/Exception/ConfigurationException.php: -------------------------------------------------------------------------------- 1 | $configurations */ 22 | public function __construct(private readonly array $configurations) 23 | { 24 | } 25 | 26 | public function getConfiguration(): Configuration 27 | { 28 | $configMap = [ 29 | 'migrations_paths' => static function ($paths, Configuration $configuration): void { 30 | foreach ($paths as $namespace => $path) { 31 | $configuration->addMigrationsDirectory($namespace, $path); 32 | } 33 | }, 34 | 'migrations' => static function ($migrations, Configuration $configuration): void { 35 | foreach ($migrations as $className) { 36 | $configuration->addMigrationClass($className); 37 | } 38 | }, 39 | 40 | 'connection' => 'setConnectionName', 41 | 'em' => 'setEntityManagerName', 42 | 43 | 'table_storage' => [ 44 | 'table_name' => 'setTableName', 45 | 'version_column_name' => 'setVersionColumnName', 46 | 'version_column_length' => static function ($value, TableMetadataStorageConfiguration $configuration): void { 47 | $configuration->setVersionColumnLength((int) $value); 48 | }, 49 | 'executed_at_column_name' => 'setExecutedAtColumnName', 50 | 'execution_time_column_name' => 'setExecutionTimeColumnName', 51 | ], 52 | 53 | 'organize_migrations' => 'setMigrationOrganization', 54 | 'custom_template' => 'setCustomTemplate', 55 | 'all_or_nothing' => static function ($value, Configuration $configuration): void { 56 | $configuration->setAllOrNothing(is_bool($value) ? $value : BooleanStringFormatter::toBoolean($value, false)); 57 | }, 58 | 'transactional' => static function ($value, Configuration $configuration): void { 59 | $configuration->setTransactional(is_bool($value) ? $value : BooleanStringFormatter::toBoolean($value, true)); 60 | }, 61 | 'check_database_platform' => static function ($value, Configuration $configuration): void { 62 | $configuration->setCheckDatabasePlatform(is_bool($value) ? $value : BooleanStringFormatter::toBoolean($value, false)); 63 | }, 64 | ]; 65 | 66 | $object = new Configuration(); 67 | self::applyConfigs($configMap, $object, $this->configurations); 68 | 69 | if ($object->getMetadataStorageConfiguration() === null) { 70 | $object->setMetadataStorageConfiguration(new TableMetadataStorageConfiguration()); 71 | } 72 | 73 | return $object; 74 | } 75 | 76 | /** 77 | * @param mixed[] $configMap 78 | * @param array $data 79 | */ 80 | private static function applyConfigs(array $configMap, Configuration|TableMetadataStorageConfiguration $object, array $data): void 81 | { 82 | foreach ($data as $configurationKey => $configurationValue) { 83 | if (! isset($configMap[$configurationKey])) { 84 | throw InvalidConfigurationKey::new((string) $configurationKey); 85 | } 86 | 87 | if (is_array($configMap[$configurationKey])) { 88 | if ($configurationKey !== 'table_storage') { 89 | throw InvalidConfigurationKey::new((string) $configurationKey); 90 | } 91 | 92 | $storageConfig = new TableMetadataStorageConfiguration(); 93 | assert($object instanceof Configuration); 94 | $object->setMetadataStorageConfiguration($storageConfig); 95 | self::applyConfigs($configMap[$configurationKey], $storageConfig, $configurationValue); 96 | } else { 97 | $callable = $configMap[$configurationKey] instanceof Closure 98 | ? $configMap[$configurationKey] 99 | : [$object, $configMap[$configurationKey]]; 100 | assert(is_callable($callable)); 101 | call_user_func( 102 | $callable, 103 | $configurationValue, 104 | $object, 105 | $data, 106 | ); 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Configuration/Migration/ConfigurationFile.php: -------------------------------------------------------------------------------- 1 | file = $file; 18 | } 19 | 20 | /** 21 | * @param array $directories 22 | * 23 | * @return array 24 | */ 25 | final protected function getDirectoriesRelativeToFile(array $directories, string $file): array 26 | { 27 | foreach ($directories as $ns => $dir) { 28 | $path = realpath(dirname($file) . '/' . $dir); 29 | 30 | $directories[$ns] = $path !== false ? $path : $dir; 31 | } 32 | 33 | return $directories; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Configuration/Migration/ConfigurationFileWithFallback.php: -------------------------------------------------------------------------------- 1 | file !== null) { 28 | return $this->loadConfiguration($this->file); 29 | } 30 | 31 | /** 32 | * If no config has been provided, look for default config file in the path. 33 | */ 34 | $defaultFiles = [ 35 | 'migrations.xml', 36 | 'migrations.yml', 37 | 'migrations.yaml', 38 | 'migrations.json', 39 | 'migrations.php', 40 | ]; 41 | 42 | foreach ($defaultFiles as $file) { 43 | if ($this->configurationFileExists($file)) { 44 | return $this->loadConfiguration($file); 45 | } 46 | } 47 | 48 | throw MissingConfigurationFile::new(); 49 | } 50 | 51 | private function configurationFileExists(string $config): bool 52 | { 53 | return file_exists($config); 54 | } 55 | 56 | /** @throws FileTypeNotSupported */ 57 | private function loadConfiguration(string $file): Configuration 58 | { 59 | return (new FormattedFile($file))->getConfiguration(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Configuration/Migration/ConfigurationLoader.php: -------------------------------------------------------------------------------- 1 | configurations; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Configuration/Migration/FormattedFile.php: -------------------------------------------------------------------------------- 1 | loaders = [ 24 | 'json' => static fn ($file): ConfigurationLoader => new JsonFile($file), 25 | 'php' => static fn ($file): ConfigurationLoader => new PhpFile($file), 26 | 'xml' => static fn ($file): ConfigurationLoader => new XmlFile($file), 27 | 'yaml' => static fn ($file): ConfigurationLoader => new YamlFile($file), 28 | 'yml' => static fn ($file): ConfigurationLoader => new YamlFile($file), 29 | ]; 30 | } 31 | 32 | public function getConfiguration(): Configuration 33 | { 34 | if (count($this->loaders) === 0) { 35 | $this->setDefaultLoaders(); 36 | } 37 | 38 | $extension = pathinfo($this->file, PATHINFO_EXTENSION); 39 | if (! isset($this->loaders[$extension])) { 40 | throw InvalidConfigurationFormat::new($this->file); 41 | } 42 | 43 | return $this->loaders[$extension]($this->file)->getConfiguration(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Configuration/Migration/JsonFile.php: -------------------------------------------------------------------------------- 1 | file)) { 24 | throw FileNotFound::new($this->file); 25 | } 26 | 27 | $contents = file_get_contents($this->file); 28 | 29 | assert($contents !== false); 30 | 31 | $config = json_decode($contents, true); 32 | 33 | if (json_last_error() !== JSON_ERROR_NONE) { 34 | throw JsonNotValid::new(); 35 | } 36 | 37 | if (isset($config['migrations_paths'])) { 38 | $config['migrations_paths'] = $this->getDirectoriesRelativeToFile( 39 | $config['migrations_paths'], 40 | $this->file, 41 | ); 42 | } 43 | 44 | return (new ConfigurationArray($config))->getConfiguration(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Configuration/Migration/PhpFile.php: -------------------------------------------------------------------------------- 1 | file)) { 19 | throw FileNotFound::new($this->file); 20 | } 21 | 22 | $config = require $this->file; 23 | if ($config instanceof Configuration) { 24 | return $config; 25 | } 26 | 27 | assert(is_array($config)); 28 | if (isset($config['migrations_paths'])) { 29 | $config['migrations_paths'] = $this->getDirectoriesRelativeToFile( 30 | $config['migrations_paths'], 31 | $this->file, 32 | ); 33 | } 34 | 35 | return (new ConfigurationArray($config))->getConfiguration(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Configuration/Migration/XML/configuration.xsd: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/Configuration/Migration/XmlFile.php: -------------------------------------------------------------------------------- 1 | file)) { 30 | throw FileNotFound::new($this->file); 31 | } 32 | 33 | $this->validateXml($this->file); 34 | 35 | $rawXML = file_get_contents($this->file); 36 | assert($rawXML !== false); 37 | 38 | $root = simplexml_load_string($rawXML, SimpleXMLElement::class, LIBXML_NOCDATA); 39 | assert($root !== false); 40 | 41 | $config = $this->extractParameters($root, true); 42 | 43 | if (isset($config['all_or_nothing'])) { 44 | $config['all_or_nothing'] = BooleanStringFormatter::toBoolean( 45 | $config['all_or_nothing'], 46 | false, 47 | ); 48 | } 49 | 50 | if (isset($config['transactional'])) { 51 | $config['transactional'] = BooleanStringFormatter::toBoolean( 52 | $config['transactional'], 53 | true, 54 | ); 55 | } 56 | 57 | if (isset($config['migrations_paths'])) { 58 | $config['migrations_paths'] = $this->getDirectoriesRelativeToFile( 59 | $config['migrations_paths'], 60 | $this->file, 61 | ); 62 | } 63 | 64 | return (new ConfigurationArray($config))->getConfiguration(); 65 | } 66 | 67 | /** @return mixed[] */ 68 | private function extractParameters(SimpleXMLElement $root, bool $loopOverNodes): array 69 | { 70 | $config = []; 71 | 72 | $itemsToCheck = $loopOverNodes ? $root->children() : $root->attributes(); 73 | 74 | if (! ($itemsToCheck instanceof SimpleXMLElement)) { 75 | return $config; 76 | } 77 | 78 | foreach ($itemsToCheck as $node) { 79 | $nodeName = strtr($node->getName(), '-', '_'); 80 | if ($nodeName === 'migrations_paths') { 81 | $config['migrations_paths'] = []; 82 | foreach ($node->path as $pathNode) { 83 | $config['migrations_paths'][(string) $pathNode['namespace']] = (string) $pathNode; 84 | } 85 | } elseif ($nodeName === 'storage' && $node->{'table-storage'} instanceof SimpleXMLElement) { 86 | $config['table_storage'] = $this->extractParameters($node->{'table-storage'}, false); 87 | } elseif ($nodeName === 'migrations') { 88 | $config['migrations'] = $this->extractMigrations($node); 89 | } else { 90 | $config[$nodeName] = (string) $node; 91 | } 92 | } 93 | 94 | return $config; 95 | } 96 | 97 | /** @return list */ 98 | private function extractMigrations(SimpleXMLElement $node): array 99 | { 100 | $migrations = []; 101 | foreach ($node->migration as $pathNode) { 102 | $migrations[] = (string) $pathNode; 103 | } 104 | 105 | return $migrations; 106 | } 107 | 108 | private function validateXml(string $file): void 109 | { 110 | try { 111 | libxml_use_internal_errors(true); 112 | 113 | $xml = new DOMDocument(); 114 | 115 | if ($xml->load($file) === false) { 116 | throw XmlNotValid::malformed(); 117 | } 118 | 119 | $xsdPath = __DIR__ . DIRECTORY_SEPARATOR . 'XML' . DIRECTORY_SEPARATOR . 'configuration.xsd'; 120 | 121 | if ($xml->schemaValidate($xsdPath) === false) { 122 | throw XmlNotValid::failedValidation(); 123 | } 124 | } finally { 125 | libxml_clear_errors(); 126 | libxml_use_internal_errors(false); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Configuration/Migration/YamlFile.php: -------------------------------------------------------------------------------- 1 | file)) { 29 | throw FileNotFound::new($this->file); 30 | } 31 | 32 | $content = file_get_contents($this->file); 33 | 34 | assert($content !== false); 35 | 36 | try { 37 | $config = Yaml::parse($content); 38 | } catch (ParseException) { 39 | throw YamlNotValid::malformed(); 40 | } 41 | 42 | if (! is_array($config)) { 43 | throw YamlNotValid::invalid(); 44 | } 45 | 46 | if (isset($config['migrations_paths'])) { 47 | $config['migrations_paths'] = $this->getDirectoriesRelativeToFile( 48 | $config['migrations_paths'], 49 | $this->file, 50 | ); 51 | } 52 | 53 | return (new ConfigurationArray($config))->getConfiguration(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/DbalMigrator.php: -------------------------------------------------------------------------------- 1 | */ 40 | private function executeMigrations( 41 | MigrationPlanList $migrationsPlan, 42 | MigratorConfiguration $migratorConfiguration, 43 | ): array { 44 | $allOrNothing = $migratorConfiguration->isAllOrNothing(); 45 | 46 | if ($allOrNothing) { 47 | $this->assertAllMigrationsAreTransactional($migrationsPlan); 48 | $this->connection->beginTransaction(); 49 | } 50 | 51 | try { 52 | $this->dispatcher->dispatchMigrationEvent(Events::onMigrationsMigrating, $migrationsPlan, $migratorConfiguration); 53 | 54 | $sql = $this->executePlan($migrationsPlan, $migratorConfiguration); 55 | 56 | $this->dispatcher->dispatchMigrationEvent(Events::onMigrationsMigrated, $migrationsPlan, $migratorConfiguration); 57 | } catch (Throwable $e) { 58 | if ($allOrNothing) { 59 | TransactionHelper::rollbackIfInTransaction($this->connection); 60 | } 61 | 62 | throw $e; 63 | } 64 | 65 | if ($allOrNothing) { 66 | TransactionHelper::commitIfInTransaction($this->connection); 67 | } 68 | 69 | return $sql; 70 | } 71 | 72 | private function assertAllMigrationsAreTransactional(MigrationPlanList $migrationsPlan): void 73 | { 74 | foreach ($migrationsPlan->getItems() as $plan) { 75 | if (! $plan->getMigration()->isTransactional()) { 76 | throw MigrationConfigurationConflict::migrationIsNotTransactional($plan->getMigration()); 77 | } 78 | } 79 | } 80 | 81 | /** @return array */ 82 | private function executePlan(MigrationPlanList $migrationsPlan, MigratorConfiguration $migratorConfiguration): array 83 | { 84 | $sql = []; 85 | 86 | foreach ($migrationsPlan->getItems() as $plan) { 87 | $versionExecutionResult = $this->executor->execute($plan, $migratorConfiguration); 88 | 89 | // capture the to Schema for the migration so we have the ability to use 90 | // it as the from Schema for the next migration when we are running a dry run 91 | // $toSchema may be null in the case of skipped migrations 92 | if (! $versionExecutionResult->isSkipped()) { 93 | $migratorConfiguration->setFromSchema($versionExecutionResult->getToSchema()); 94 | } 95 | 96 | $sql[(string) $plan->getVersion()] = $versionExecutionResult->getSql(); 97 | } 98 | 99 | return $sql; 100 | } 101 | 102 | /** @param array $sql */ 103 | private function endMigrations( 104 | StopwatchEvent $stopwatchEvent, 105 | MigrationPlanList $migrationsPlan, 106 | array $sql, 107 | ): void { 108 | $stopwatchEvent->stop(); 109 | 110 | $this->logger->notice( 111 | 'finished in {duration}ms, used {memory} memory, {migrations_count} migrations executed, {queries_count} sql queries', 112 | [ 113 | 'duration' => $stopwatchEvent->getDuration(), 114 | 'memory' => BytesFormatter::formatBytes($stopwatchEvent->getMemory()), 115 | 'migrations_count' => count($migrationsPlan), 116 | 'queries_count' => count($sql, COUNT_RECURSIVE) - count($sql), 117 | ], 118 | ); 119 | } 120 | 121 | /** 122 | * {@inheritDoc} 123 | */ 124 | public function migrate(MigrationPlanList $migrationsPlan, MigratorConfiguration $migratorConfiguration): array 125 | { 126 | if (count($migrationsPlan) === 0) { 127 | $this->logger->notice('No migrations to execute.'); 128 | 129 | return []; 130 | } 131 | 132 | $stopwatchEvent = $this->stopwatch->start('migrate'); 133 | 134 | $sql = $this->executeMigrations($migrationsPlan, $migratorConfiguration); 135 | 136 | $this->endMigrations($stopwatchEvent, $migrationsPlan, $sql); 137 | 138 | return $sql; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Event/Listeners/AutoCommitListener.php: -------------------------------------------------------------------------------- 1 | getConnection(); 23 | $conf = $args->getMigratorConfiguration(); 24 | 25 | if ($conf->isDryRun() || $conn->isAutoCommit()) { 26 | return; 27 | } 28 | 29 | TransactionHelper::commitIfInTransaction($conn); 30 | } 31 | 32 | /** {@inheritDoc} */ 33 | public function getSubscribedEvents() 34 | { 35 | return [Events::onMigrationsMigrated]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Event/MigrationsEventArgs.php: -------------------------------------------------------------------------------- 1 | connection; 27 | } 28 | 29 | public function getPlan(): MigrationPlanList 30 | { 31 | return $this->plan; 32 | } 33 | 34 | public function getMigratorConfiguration(): MigratorConfiguration 35 | { 36 | return $this->migratorConfiguration; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Event/MigrationsVersionEventArgs.php: -------------------------------------------------------------------------------- 1 | connection; 27 | } 28 | 29 | public function getPlan(): MigrationPlan 30 | { 31 | return $this->plan; 32 | } 33 | 34 | public function getMigratorConfiguration(): MigratorConfiguration 35 | { 36 | return $this->migratorConfiguration; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/EventDispatcher.php: -------------------------------------------------------------------------------- 1 | createMigrationEventArgs($migrationsPlan, $migratorConfiguration); 34 | 35 | $this->dispatchEvent($eventName, $event); 36 | } 37 | 38 | public function dispatchVersionEvent( 39 | string $eventName, 40 | MigrationPlan $plan, 41 | MigratorConfiguration $migratorConfiguration, 42 | ): void { 43 | $event = $this->createMigrationsVersionEventArgs( 44 | $plan, 45 | $migratorConfiguration, 46 | ); 47 | 48 | $this->dispatchEvent($eventName, $event); 49 | } 50 | 51 | private function dispatchEvent(string $eventName, EventArgs|null $args = null): void 52 | { 53 | $this->eventManager->dispatchEvent($eventName, $args); 54 | } 55 | 56 | private function createMigrationEventArgs( 57 | MigrationPlanList $migrationsPlan, 58 | MigratorConfiguration $migratorConfiguration, 59 | ): MigrationsEventArgs { 60 | return new MigrationsEventArgs($this->connection, $migrationsPlan, $migratorConfiguration); 61 | } 62 | 63 | private function createMigrationsVersionEventArgs( 64 | MigrationPlan $plan, 65 | MigratorConfiguration $migratorConfiguration, 66 | ): MigrationsVersionEventArgs { 67 | return new MigrationsVersionEventArgs( 68 | $this->connection, 69 | $plan, 70 | $migratorConfiguration, 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Events.php: -------------------------------------------------------------------------------- 1 | $queriesByVersion */ 31 | public function write( 32 | string $path, 33 | string $direction, 34 | array $queriesByVersion, 35 | DateTimeInterface|null $now = null, 36 | ): bool { 37 | $now ??= new DateTimeImmutable(); 38 | 39 | $string = $this->migrationFileBuilder 40 | ->buildMigrationFile($queriesByVersion, $direction, $now); 41 | 42 | $path = $this->buildMigrationFilePath($path, $now); 43 | 44 | $this->logger->info('Writing migration file to "{path}"', ['path' => $path]); 45 | 46 | return file_put_contents($path, $string) !== false; 47 | } 48 | 49 | private function buildMigrationFilePath(string $path, DateTimeInterface $now): string 50 | { 51 | if (is_dir($path)) { 52 | $path = realpath($path); 53 | $path .= '/doctrine_migration_' . $now->format('YmdHis') . '.sql'; 54 | } 55 | 56 | return $path; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/FilesystemMigrationsRepository.php: -------------------------------------------------------------------------------- 1 | $migrationDirectories 34 | */ 35 | public function __construct( 36 | array $classes, 37 | private readonly array $migrationDirectories, 38 | private readonly MigrationFinder $migrationFinder, 39 | private readonly MigrationFactory $versionFactory, 40 | ) { 41 | $this->registerMigrations($classes); 42 | } 43 | 44 | private function registerMigrationInstance(Version $version, AbstractMigration $migration): AvailableMigration 45 | { 46 | if (isset($this->migrations[(string) $version])) { 47 | throw DuplicateMigrationVersion::new( 48 | (string) $version, 49 | (string) $version, 50 | ); 51 | } 52 | 53 | $this->migrations[(string) $version] = new AvailableMigration($version, $migration); 54 | 55 | return $this->migrations[(string) $version]; 56 | } 57 | 58 | /** @throws MigrationException */ 59 | public function registerMigration(string $migrationClassName): AvailableMigration 60 | { 61 | $this->ensureMigrationClassExists($migrationClassName); 62 | 63 | $version = new Version($migrationClassName); 64 | $migration = $this->versionFactory->createVersion($migrationClassName); 65 | 66 | return $this->registerMigrationInstance($version, $migration); 67 | } 68 | 69 | /** 70 | * @param string[] $migrations 71 | * 72 | * @return AvailableMigration[] 73 | */ 74 | private function registerMigrations(array $migrations): array 75 | { 76 | $versions = []; 77 | 78 | foreach ($migrations as $class) { 79 | $versions[] = $this->registerMigration($class); 80 | } 81 | 82 | return $versions; 83 | } 84 | 85 | public function hasMigration(string $version): bool 86 | { 87 | $this->loadMigrationsFromDirectories(); 88 | 89 | return isset($this->migrations[$version]); 90 | } 91 | 92 | public function getMigration(Version $version): AvailableMigration 93 | { 94 | $this->loadMigrationsFromDirectories(); 95 | 96 | if (! isset($this->migrations[(string) $version])) { 97 | throw MigrationClassNotFound::new((string) $version); 98 | } 99 | 100 | return $this->migrations[(string) $version]; 101 | } 102 | 103 | /** 104 | * Returns a non-sorted set of migrations. 105 | */ 106 | public function getMigrations(): AvailableMigrationsSet 107 | { 108 | $this->loadMigrationsFromDirectories(); 109 | 110 | return new AvailableMigrationsSet($this->migrations); 111 | } 112 | 113 | /** @throws MigrationException */ 114 | private function ensureMigrationClassExists(string $class): void 115 | { 116 | if (! class_exists($class)) { 117 | throw MigrationClassNotFound::new($class); 118 | } 119 | } 120 | 121 | private function loadMigrationsFromDirectories(): void 122 | { 123 | $migrationDirectories = $this->migrationDirectories; 124 | 125 | if ($this->migrationsLoaded) { 126 | return; 127 | } 128 | 129 | $this->migrationsLoaded = true; 130 | 131 | foreach ($migrationDirectories as $namespace => $path) { 132 | $migrations = $this->migrationFinder->findMigrations( 133 | $path, 134 | $namespace, 135 | ); 136 | $this->registerMigrations($migrations); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Finder/Exception/FinderException.php: -------------------------------------------------------------------------------- 1 | loadMigrationClasses($includedFiles, $namespace); 61 | $versions = []; 62 | foreach ($classes as $class) { 63 | $versions[] = $class->getName(); 64 | } 65 | 66 | return $versions; 67 | } 68 | 69 | /** 70 | * Look up all declared classes and find those classes contained 71 | * in the given `$files` array. 72 | * 73 | * @param string[] $files The set of files that were `required` 74 | * @param string|null $namespace If not null only classes in this namespace will be returned 75 | * 76 | * @return ReflectionClass[] the classes in `$files` 77 | */ 78 | protected function loadMigrationClasses(array $files, string|null $namespace = null): array 79 | { 80 | $classes = []; 81 | foreach (get_declared_classes() as $class) { 82 | $reflectionClass = new ReflectionClass($class); 83 | 84 | if (! in_array($reflectionClass->getFileName(), $files, true)) { 85 | continue; 86 | } 87 | 88 | if ($namespace !== null && ! $this->isReflectionClassInNamespace($reflectionClass, $namespace)) { 89 | continue; 90 | } 91 | 92 | $classes[] = $reflectionClass; 93 | } 94 | 95 | return $classes; 96 | } 97 | 98 | /** @param ReflectionClass $reflectionClass */ 99 | private function isReflectionClassInNamespace(ReflectionClass $reflectionClass, string $namespace): bool 100 | { 101 | return strncmp($reflectionClass->getName(), $namespace . '\\', strlen($namespace) + 1) === 0; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Finder/GlobFinder.php: -------------------------------------------------------------------------------- 1 | getRealPath($directory); 21 | 22 | $files = glob(rtrim($dir, '/') . '/Version*.php'); 23 | if ($files === false) { 24 | $files = []; 25 | } 26 | 27 | return $this->loadMigrations($files, $namespace); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Finder/MigrationFinder.php: -------------------------------------------------------------------------------- 1 | pattern = $pattern ?? sprintf( 26 | '#^.+\\%s[^\\%s]+\\.php$#i', 27 | DIRECTORY_SEPARATOR, 28 | DIRECTORY_SEPARATOR, 29 | ); 30 | } 31 | 32 | /** @return string[] */ 33 | public function findMigrations(string $directory, string|null $namespace = null): array 34 | { 35 | $dir = $this->getRealPath($directory); 36 | 37 | return $this->loadMigrations( 38 | $this->getMatches($this->createIterator($dir)), 39 | $namespace, 40 | ); 41 | } 42 | 43 | private function createIterator(string $dir): RegexIterator 44 | { 45 | return new RegexIterator( 46 | new RecursiveIteratorIterator( 47 | new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS), 48 | RecursiveIteratorIterator::LEAVES_ONLY, 49 | ), 50 | $this->getPattern(), 51 | RegexIterator::GET_MATCH, 52 | ); 53 | } 54 | 55 | private function getPattern(): string 56 | { 57 | return $this->pattern; 58 | } 59 | 60 | /** @return string[] */ 61 | private function getMatches(RegexIterator $iteratorFilesMatch): array 62 | { 63 | $files = []; 64 | foreach ($iteratorFilesMatch as $file) { 65 | $files[] = $file[0]; 66 | } 67 | 68 | return $files; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Generator/ClassNameGenerator.php: -------------------------------------------------------------------------------- 1 | generateVersionNumber(); 17 | } 18 | 19 | private function generateVersionNumber(): string 20 | { 21 | $now = new DateTimeImmutable('now', new DateTimeZone('UTC')); 22 | 23 | return $now->format(self::VERSION_FORMAT); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Generator/ConcatenationFileBuilder.php: -------------------------------------------------------------------------------- 1 | $queriesByVersion */ 21 | public function buildMigrationFile( 22 | array $queriesByVersion, 23 | string $direction, 24 | DateTimeInterface|null $now = null, 25 | ): string { 26 | $now ??= new DateTimeImmutable(); 27 | $string = sprintf("-- Doctrine Migration File Generated on %s\n", $now->format('Y-m-d H:i:s')); 28 | 29 | foreach ($queriesByVersion as $version => $queries) { 30 | $string .= "\n-- Version " . $version . "\n"; 31 | 32 | foreach ($queries as $query) { 33 | $string .= $query->getStatement() . ";\n"; 34 | } 35 | } 36 | 37 | return $string; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Generator/DiffGenerator.php: -------------------------------------------------------------------------------- 1 | $schemaManager */ 29 | public function __construct( 30 | private readonly DBALConfiguration $dbalConfiguration, 31 | private readonly AbstractSchemaManager $schemaManager, 32 | private readonly SchemaProvider $schemaProvider, 33 | private readonly AbstractPlatform $platform, 34 | private readonly Generator $migrationGenerator, 35 | private readonly SqlGenerator $migrationSqlGenerator, 36 | private readonly SchemaProvider $emptySchemaProvider, 37 | ) { 38 | } 39 | 40 | /** @throws NoChangesDetected */ 41 | public function generate( 42 | string $fqcn, 43 | string|null $filterExpression, 44 | bool $formatted = false, 45 | int $lineLength = 120, 46 | bool $checkDbPlatform = true, 47 | bool $fromEmptySchema = false, 48 | ): string { 49 | if ($filterExpression !== null) { 50 | $this->dbalConfiguration->setSchemaAssetsFilter( 51 | static function ($assetName) use ($filterExpression) { 52 | if ($assetName instanceof AbstractAsset) { 53 | $assetName = $assetName->getName(); 54 | } 55 | 56 | return preg_match($filterExpression, $assetName); 57 | }, 58 | ); 59 | } 60 | 61 | $fromSchema = $fromEmptySchema 62 | ? $this->createEmptySchema() 63 | : $this->createFromSchema(); 64 | 65 | $toSchema = $this->createToSchema(); 66 | 67 | // prior to DBAL 4.0, the schema name was set to the first element in the search path, 68 | // which is not necessarily the default schema name 69 | if ( 70 | ! method_exists($this->schemaManager, 'getSchemaSearchPaths') 71 | && $this->platform->supportsSchemas() 72 | ) { 73 | $defaultNamespace = $toSchema->getName(); 74 | if ($defaultNamespace !== '') { 75 | $toSchema->createNamespace($defaultNamespace); 76 | } 77 | } 78 | 79 | $comparator = $this->schemaManager->createComparator(); 80 | 81 | $upSql = $this->platform->getAlterSchemaSQL($comparator->compareSchemas($fromSchema, $toSchema)); 82 | 83 | $up = $this->migrationSqlGenerator->generate( 84 | $upSql, 85 | $formatted, 86 | $lineLength, 87 | $checkDbPlatform, 88 | ); 89 | 90 | $downSql = $this->platform->getAlterSchemaSQL($comparator->compareSchemas($toSchema, $fromSchema)); 91 | 92 | $down = $this->migrationSqlGenerator->generate( 93 | $downSql, 94 | $formatted, 95 | $lineLength, 96 | $checkDbPlatform, 97 | ); 98 | 99 | if ($up === '' && $down === '') { 100 | throw NoChangesDetected::new(); 101 | } 102 | 103 | return $this->migrationGenerator->generateMigration( 104 | $fqcn, 105 | $up, 106 | $down, 107 | ); 108 | } 109 | 110 | private function createEmptySchema(): Schema 111 | { 112 | return $this->emptySchemaProvider->createSchema(); 113 | } 114 | 115 | private function createFromSchema(): Schema 116 | { 117 | return $this->schemaManager->introspectSchema(); 118 | } 119 | 120 | private function createToSchema(): Schema 121 | { 122 | $toSchema = $this->schemaProvider->createSchema(); 123 | 124 | $schemaAssetsFilter = $this->dbalConfiguration->getSchemaAssetsFilter(); 125 | 126 | if ($schemaAssetsFilter !== null) { 127 | foreach ($toSchema->getTables() as $table) { 128 | $tableName = $table->getName(); 129 | 130 | if ($schemaAssetsFilter($this->resolveTableName($tableName))) { 131 | continue; 132 | } 133 | 134 | $toSchema->dropTable($tableName); 135 | } 136 | } 137 | 138 | return $toSchema; 139 | } 140 | 141 | /** 142 | * Resolve a table name from its fully qualified name. The `$name` argument 143 | * comes from Doctrine\DBAL\Schema\Table#getName which can sometimes return 144 | * a namespaced name with the form `{namespace}.{tableName}`. This extracts 145 | * the table name from that. 146 | */ 147 | private function resolveTableName(string $name): string 148 | { 149 | $pos = strpos($name, '.'); 150 | 151 | return $pos === false ? $name : substr($name, $pos + 1); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Generator/Exception/GeneratorException.php: -------------------------------------------------------------------------------- 1 | $queriesByVersion */ 18 | public function buildMigrationFile(array $queriesByVersion, string $direction, DateTimeInterface|null $now = null): string; 19 | } 20 | -------------------------------------------------------------------------------- /src/Generator/Generator.php: -------------------------------------------------------------------------------- 1 | ; 37 | 38 | use Doctrine\DBAL\Schema\Schema; 39 | use Doctrine\Migrations\AbstractMigration; 40 | 41 | /** 42 | * Auto-generated Migration: Please modify to your needs! 43 | */ 44 | final class extends AbstractMigration 45 | { 46 | public function getDescription(): string 47 | { 48 | return ''; 49 | } 50 | 51 | public function up(Schema $schema): void 52 | { 53 | // this up() migration is auto-generated, please modify it to your needs 54 | 55 | } 56 | 57 | public function down(Schema $schema): void 58 | { 59 | // this down() migration is auto-generated, please modify it to your needs 60 | 61 | } 62 | } 63 | 64 | TEMPLATE; 65 | 66 | private string|null $template = null; 67 | 68 | public function __construct(private readonly Configuration $configuration) 69 | { 70 | } 71 | 72 | public function generateMigration( 73 | string $fqcn, 74 | string|null $up = null, 75 | string|null $down = null, 76 | ): string { 77 | $mch = []; 78 | if (preg_match('~(.*)\\\\([^\\\\]+)~', $fqcn, $mch) !== 1) { 79 | throw new InvalidArgumentException(sprintf('Invalid FQCN')); 80 | } 81 | 82 | [$fqcn, $namespace, $className] = $mch; 83 | 84 | $dirs = $this->configuration->getMigrationDirectories(); 85 | if (! isset($dirs[$namespace])) { 86 | throw new InvalidArgumentException(sprintf('Path not defined for the namespace "%s"', $namespace)); 87 | } 88 | 89 | $dir = $dirs[$namespace]; 90 | 91 | $replacements = [ 92 | '' => $namespace, 93 | '' => $className, 94 | '' => $up !== null ? ' ' . implode("\n ", explode("\n", $up)) : null, 95 | '' => $down !== null ? ' ' . implode("\n ", explode("\n", $down)) : null, 96 | '' => $this->configuration->isTransactional() ? '' : <<<'METHOD' 97 | 98 | 99 | public function isTransactional(): bool 100 | { 101 | return false; 102 | } 103 | METHOD 104 | , 105 | ]; 106 | 107 | $code = strtr($this->getTemplate(), $replacements); 108 | $code = preg_replace('/^ +$/m', '', $code); 109 | 110 | $directoryHelper = new MigrationDirectoryHelper(); 111 | $dir = $directoryHelper->getMigrationDirectory($this->configuration, $dir); 112 | $path = $dir . '/' . $className . '.php'; 113 | 114 | file_put_contents($path, $code); 115 | 116 | return $path; 117 | } 118 | 119 | private function getTemplate(): string 120 | { 121 | if ($this->template === null) { 122 | $this->template = $this->loadCustomTemplate(); 123 | 124 | if ($this->template === null) { 125 | $this->template = self::MIGRATION_TEMPLATE; 126 | } 127 | } 128 | 129 | return $this->template; 130 | } 131 | 132 | /** @throws InvalidTemplateSpecified */ 133 | private function loadCustomTemplate(): string|null 134 | { 135 | $customTemplate = $this->configuration->getCustomTemplate(); 136 | 137 | if ($customTemplate === null) { 138 | return null; 139 | } 140 | 141 | if (! is_file($customTemplate) || ! is_readable($customTemplate)) { 142 | throw InvalidTemplateSpecified::notFoundOrNotReadable($customTemplate); 143 | } 144 | 145 | $content = file_get_contents($customTemplate); 146 | 147 | if ($content === false) { 148 | throw InvalidTemplateSpecified::notReadable($customTemplate); 149 | } 150 | 151 | if (trim($content) === '') { 152 | throw InvalidTemplateSpecified::empty($customTemplate); 153 | } 154 | 155 | return $content; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Generator/SqlGenerator.php: -------------------------------------------------------------------------------- 1 | configuration->getMetadataStorageConfiguration(); 49 | foreach ($sql as $query) { 50 | if ( 51 | $storageConfiguration instanceof TableMetadataStorageConfiguration 52 | && stripos($query, $storageConfiguration->getTableName()) !== false 53 | ) { 54 | continue; 55 | } 56 | 57 | if ($formatted) { 58 | $maxLength = $lineLength - 18 - 8; // max - php code length - indentation 59 | 60 | if (strlen($query) > $maxLength) { 61 | $query = $this->formatQuery($query); 62 | } 63 | } 64 | 65 | $code[] = sprintf( 66 | "\$this->addSql(<<<'SQL'\n%s\nSQL);", 67 | preg_replace('/^/m', str_repeat(' ', 4), $query), 68 | ); 69 | } 70 | 71 | if (count($code) !== 0 && $checkDbPlatform && $this->configuration->isDatabasePlatformChecked()) { 72 | $currentPlatform = '\\' . get_class($this->platform); 73 | 74 | array_unshift( 75 | $code, 76 | sprintf( 77 | <<<'PHP' 78 | $this->abortIf( 79 | !$this->connection->getDatabasePlatform() instanceof %s, 80 | "Migration can only be executed safely on '%s'." 81 | ); 82 | PHP 83 | , 84 | $currentPlatform, 85 | $currentPlatform, 86 | ), 87 | '', 88 | ); 89 | } 90 | 91 | return implode("\n", $code); 92 | } 93 | 94 | private function formatQuery(string $query): string 95 | { 96 | $this->formatter ??= new SqlFormatter(new NullHighlighter()); 97 | 98 | return $this->formatter->format($query); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/InlineParameterFormatter.php: -------------------------------------------------------------------------------- 1 | $value) { 44 | $type = $types[$key] ?? 'string'; 45 | 46 | $formattedParameter = '[' . $this->formatParameter($value, $type) . ']'; 47 | 48 | $formattedParameters[] = is_string($key) 49 | ? sprintf(':%s => %s', $key, $formattedParameter) 50 | : $formattedParameter; 51 | } 52 | 53 | return sprintf('with parameters (%s)', implode(', ', $formattedParameters)); 54 | } 55 | 56 | private function formatParameter(mixed $value, mixed $type): string|int|bool|float|null 57 | { 58 | if (is_string($type) && Type::hasType($type)) { 59 | return Type::getType($type)->convertToDatabaseValue( 60 | $value, 61 | $this->connection->getDatabasePlatform(), 62 | ); 63 | } 64 | 65 | return $this->parameterToString($value); 66 | } 67 | 68 | /** @param int[]|bool[]|string[]|float[]|array|int|string|float|bool $value */ 69 | private function parameterToString(array|int|string|float|bool $value): string 70 | { 71 | if (is_array($value)) { 72 | return implode(', ', array_map($this->parameterToString(...), $value)); 73 | } 74 | 75 | if (is_int($value) || is_string($value) || is_float($value)) { 76 | return (string) $value; 77 | } 78 | 79 | if (is_bool($value)) { 80 | return $value === true ? 'true' : 'false'; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Metadata/AvailableMigration.php: -------------------------------------------------------------------------------- 1 | version; 25 | } 26 | 27 | public function getMigration(): AbstractMigration 28 | { 29 | return $this->migration; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Metadata/AvailableMigrationsList.php: -------------------------------------------------------------------------------- 1 | items = array_values($items); 28 | } 29 | 30 | /** @return AvailableMigration[] */ 31 | public function getItems(): array 32 | { 33 | return $this->items; 34 | } 35 | 36 | public function getFirst(int $offset = 0): AvailableMigration 37 | { 38 | if (! isset($this->items[$offset])) { 39 | throw NoMigrationsFoundWithCriteria::new('first' . ($offset > 0 ? '+' . $offset : '')); 40 | } 41 | 42 | return $this->items[$offset]; 43 | } 44 | 45 | public function getLast(int $offset = 0): AvailableMigration 46 | { 47 | $offset = count($this->items) - 1 - (-1 * $offset); 48 | if (! isset($this->items[$offset])) { 49 | throw NoMigrationsFoundWithCriteria::new('last' . ($offset > 0 ? '+' . $offset : '')); 50 | } 51 | 52 | return $this->items[$offset]; 53 | } 54 | 55 | public function count(): int 56 | { 57 | return count($this->items); 58 | } 59 | 60 | public function hasMigration(Version $version): bool 61 | { 62 | foreach ($this->items as $migration) { 63 | if ($migration->getVersion()->equals($version)) { 64 | return true; 65 | } 66 | } 67 | 68 | return false; 69 | } 70 | 71 | public function getMigration(Version $version): AvailableMigration 72 | { 73 | foreach ($this->items as $migration) { 74 | if ($migration->getVersion()->equals($version)) { 75 | return $migration; 76 | } 77 | } 78 | 79 | throw MigrationNotAvailable::forVersion($version); 80 | } 81 | 82 | public function newSubset(ExecutedMigrationsList $executedMigrations): self 83 | { 84 | return new self(array_filter($this->getItems(), static fn (AvailableMigration $migration): bool => ! $executedMigrations->hasMigration($migration->getVersion()))); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Metadata/AvailableMigrationsSet.php: -------------------------------------------------------------------------------- 1 | items = array_values($items); 26 | } 27 | 28 | /** @return AvailableMigration[] */ 29 | public function getItems(): array 30 | { 31 | return $this->items; 32 | } 33 | 34 | public function count(): int 35 | { 36 | return count($this->items); 37 | } 38 | 39 | public function hasMigration(Version $version): bool 40 | { 41 | foreach ($this->items as $migration) { 42 | if ($migration->getVersion()->equals($version)) { 43 | return true; 44 | } 45 | } 46 | 47 | return false; 48 | } 49 | 50 | public function getMigration(Version $version): AvailableMigration 51 | { 52 | foreach ($this->items as $migration) { 53 | if ($migration->getVersion()->equals($version)) { 54 | return $migration; 55 | } 56 | } 57 | 58 | throw MigrationNotAvailable::forVersion($version); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Metadata/ExecutedMigration.php: -------------------------------------------------------------------------------- 1 | executionTime; 26 | } 27 | 28 | public function getExecutedAt(): DateTimeImmutable|null 29 | { 30 | return $this->executedAt; 31 | } 32 | 33 | public function getVersion(): Version 34 | { 35 | return $this->version; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Metadata/ExecutedMigrationsList.php: -------------------------------------------------------------------------------- 1 | items = array_values($items); 29 | } 30 | 31 | /** @return ExecutedMigration[] */ 32 | public function getItems(): array 33 | { 34 | return $this->items; 35 | } 36 | 37 | public function getFirst(int $offset = 0): ExecutedMigration 38 | { 39 | if (! isset($this->items[$offset])) { 40 | throw NoMigrationsFoundWithCriteria::new('first' . ($offset > 0 ? '+' . $offset : '')); 41 | } 42 | 43 | return $this->items[$offset]; 44 | } 45 | 46 | public function getLast(int $offset = 0): ExecutedMigration 47 | { 48 | $offset = count($this->items) - 1 - (-1 * $offset); 49 | if (! isset($this->items[$offset])) { 50 | throw NoMigrationsFoundWithCriteria::new('last' . ($offset > 0 ? '+' . $offset : '')); 51 | } 52 | 53 | return $this->items[$offset]; 54 | } 55 | 56 | public function count(): int 57 | { 58 | return count($this->items); 59 | } 60 | 61 | public function hasMigration(Version $version): bool 62 | { 63 | foreach ($this->items as $migration) { 64 | if ($migration->getVersion()->equals($version)) { 65 | return true; 66 | } 67 | } 68 | 69 | return false; 70 | } 71 | 72 | public function getMigration(Version $version): ExecutedMigration 73 | { 74 | foreach ($this->items as $migration) { 75 | if ($migration->getVersion()->equals($version)) { 76 | return $migration; 77 | } 78 | } 79 | 80 | throw MigrationNotExecuted::new((string) $version); 81 | } 82 | 83 | public function unavailableSubset(AvailableMigrationsList $availableMigrations): self 84 | { 85 | return new self(array_filter($this->getItems(), static fn (ExecutedMigration $migration): bool => ! $availableMigrations->hasMigration($migration->getVersion()))); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Metadata/MigrationPlan.php: -------------------------------------------------------------------------------- 1 | version; 29 | } 30 | 31 | public function getResult(): ExecutionResult|null 32 | { 33 | return $this->result; 34 | } 35 | 36 | public function markAsExecuted(ExecutionResult $result): void 37 | { 38 | if ($this->result !== null) { 39 | throw PlanAlreadyExecuted::new(); 40 | } 41 | 42 | $this->result = $result; 43 | } 44 | 45 | public function getMigration(): AbstractMigration 46 | { 47 | return $this->migration; 48 | } 49 | 50 | public function getDirection(): string 51 | { 52 | return $this->direction; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Metadata/MigrationPlanList.php: -------------------------------------------------------------------------------- 1 | items); 29 | } 30 | 31 | /** @return MigrationPlan[] */ 32 | public function getItems(): array 33 | { 34 | return $this->items; 35 | } 36 | 37 | public function getDirection(): string 38 | { 39 | return $this->direction; 40 | } 41 | 42 | public function getFirst(): MigrationPlan 43 | { 44 | if (count($this->items) === 0) { 45 | throw NoMigrationsFoundWithCriteria::new('first'); 46 | } 47 | 48 | return reset($this->items); 49 | } 50 | 51 | public function getLast(): MigrationPlan 52 | { 53 | if (count($this->items) === 0) { 54 | throw NoMigrationsFoundWithCriteria::new('last'); 55 | } 56 | 57 | return end($this->items); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Metadata/Storage/MetadataStorage.php: -------------------------------------------------------------------------------- 1 | getSql(ExecutionResult $result); */ 12 | interface MetadataStorage 13 | { 14 | public function ensureInitialized(): void; 15 | 16 | public function getExecutedMigrations(): ExecutedMigrationsList; 17 | 18 | public function complete(ExecutionResult $result): void; 19 | 20 | public function reset(): void; 21 | } 22 | -------------------------------------------------------------------------------- /src/Metadata/Storage/MetadataStorageConfiguration.php: -------------------------------------------------------------------------------- 1 | tableName; 22 | } 23 | 24 | public function setTableName(string $tableName): void 25 | { 26 | $this->tableName = $tableName; 27 | } 28 | 29 | public function getVersionColumnName(): string 30 | { 31 | return $this->versionColumnName; 32 | } 33 | 34 | public function setVersionColumnName(string $versionColumnName): void 35 | { 36 | $this->versionColumnName = $versionColumnName; 37 | } 38 | 39 | public function getVersionColumnLength(): int 40 | { 41 | return $this->versionColumnLength; 42 | } 43 | 44 | public function setVersionColumnLength(int $versionColumnLength): void 45 | { 46 | $this->versionColumnLength = $versionColumnLength; 47 | } 48 | 49 | public function getExecutedAtColumnName(): string 50 | { 51 | return $this->executedAtColumnName; 52 | } 53 | 54 | public function setExecutedAtColumnName(string $executedAtColumnName): void 55 | { 56 | $this->executedAtColumnName = $executedAtColumnName; 57 | } 58 | 59 | public function getExecutionTimeColumnName(): string 60 | { 61 | return $this->executionTimeColumnName; 62 | } 63 | 64 | public function setExecutionTimeColumnName(string $executionTimeColumnName): void 65 | { 66 | $this->executionTimeColumnName = $executionTimeColumnName; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/MigrationsRepository.php: -------------------------------------------------------------------------------- 1 | A list of SQL statements executed, grouped by migration version */ 18 | public function migrate(MigrationPlanList $migrationsPlan, MigratorConfiguration $migratorConfiguration): array; 19 | } 20 | -------------------------------------------------------------------------------- /src/MigratorConfiguration.php: -------------------------------------------------------------------------------- 1 | dryRun; 32 | } 33 | 34 | public function setDryRun(bool $dryRun): self 35 | { 36 | $this->dryRun = $dryRun; 37 | 38 | return $this; 39 | } 40 | 41 | public function getTimeAllQueries(): bool 42 | { 43 | return $this->timeAllQueries; 44 | } 45 | 46 | public function setTimeAllQueries(bool $timeAllQueries): self 47 | { 48 | $this->timeAllQueries = $timeAllQueries; 49 | 50 | return $this; 51 | } 52 | 53 | public function getNoMigrationException(): bool 54 | { 55 | return $this->noMigrationException; 56 | } 57 | 58 | public function setNoMigrationException(bool $noMigrationException = false): self 59 | { 60 | $this->noMigrationException = $noMigrationException; 61 | 62 | return $this; 63 | } 64 | 65 | public function isAllOrNothing(): bool 66 | { 67 | return $this->allOrNothing; 68 | } 69 | 70 | public function setAllOrNothing(bool $allOrNothing): self 71 | { 72 | $this->allOrNothing = $allOrNothing; 73 | 74 | return $this; 75 | } 76 | 77 | public function getFromSchema(): Schema|null 78 | { 79 | return $this->fromSchema; 80 | } 81 | 82 | public function setFromSchema(Schema $fromSchema): self 83 | { 84 | $this->fromSchema = $fromSchema; 85 | 86 | return $this; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/ParameterFormatter.php: -------------------------------------------------------------------------------- 1 | $schemaManager- */ 24 | public function __construct( 25 | private readonly AbstractSchemaManager $schemaManager, 26 | private readonly AbstractPlatform $platform, 27 | ) { 28 | } 29 | 30 | public function createFromSchema(): Schema 31 | { 32 | return $this->schemaManager->introspectSchema(); 33 | } 34 | 35 | public function createToSchema(Schema $fromSchema): Schema 36 | { 37 | return clone $fromSchema; 38 | } 39 | 40 | /** @return string[] */ 41 | public function getSqlDiffToMigrate(Schema $fromSchema, Schema $toSchema): array 42 | { 43 | return $this->platform->getAlterSchemaSQL( 44 | $this->schemaManager->createComparator()->compareSchemas($fromSchema, $toSchema), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Provider/EmptySchemaProvider.php: -------------------------------------------------------------------------------- 1 | $schemaManager */ 20 | public function __construct(private readonly AbstractSchemaManager $schemaManager) 21 | { 22 | } 23 | 24 | public function createSchema(): Schema 25 | { 26 | return new Schema([], [], $this->schemaManager->createSchemaConfig()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Provider/Exception/NoMappingFound.php: -------------------------------------------------------------------------------- 1 | originalSchemaManipulator; 25 | 26 | return LazySchema::createLazyProxy(static fn () => $originalSchemaManipulator->createFromSchema()); 27 | } 28 | 29 | public function createToSchema(Schema $fromSchema): Schema 30 | { 31 | $originalSchemaManipulator = $this->originalSchemaManipulator; 32 | 33 | if ($fromSchema instanceof LazySchema && ! $fromSchema->isLazyObjectInitialized()) { 34 | return LazySchema::createLazyProxy(static fn () => $originalSchemaManipulator->createToSchema($fromSchema)); 35 | } 36 | 37 | return $this->originalSchemaManipulator->createToSchema($fromSchema); 38 | } 39 | 40 | /** @return string[] */ 41 | public function getSqlDiffToMigrate(Schema $fromSchema, Schema $toSchema): array 42 | { 43 | if ( 44 | $toSchema instanceof LazySchema 45 | && ! $toSchema->isLazyObjectInitialized() 46 | ) { 47 | return []; 48 | } 49 | 50 | return $this->originalSchemaManipulator->getSqlDiffToMigrate($fromSchema, $toSchema); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Provider/OrmSchemaProvider.php: -------------------------------------------------------------------------------- 1 | entityManager = $em; 28 | } 29 | 30 | public function createSchema(): Schema 31 | { 32 | /** @var array> $metadata */ 33 | $metadata = $this->entityManager->getMetadataFactory()->getAllMetadata(); 34 | 35 | usort($metadata, static fn (ClassMetadata $a, ClassMetadata $b): int => $a->getTableName() <=> $b->getTableName()); 36 | 37 | $tool = new SchemaTool($this->entityManager); 38 | 39 | return $tool->getSchemaFromMetadata($metadata); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Provider/SchemaDiffProvider.php: -------------------------------------------------------------------------------- 1 | toSchema = $schema; 20 | } 21 | 22 | public function createSchema(): Schema 23 | { 24 | return $this->toSchema; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Query/Exception/InvalidArguments.php: -------------------------------------------------------------------------------- 1 | count($parameters)) { 27 | throw InvalidArguments::wrongTypesArgumentCount($statement, count($parameters), count($types)); 28 | } 29 | } 30 | 31 | public function __toString(): string 32 | { 33 | return $this->statement; 34 | } 35 | 36 | public function getStatement(): string 37 | { 38 | return $this->statement; 39 | } 40 | 41 | /** @return mixed[] */ 42 | public function getParameters(): array 43 | { 44 | return $this->parameters; 45 | } 46 | 47 | /** @return mixed[] */ 48 | public function getTypes(): array 49 | { 50 | return $this->types; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/QueryWriter.php: -------------------------------------------------------------------------------- 1 | $queriesByVersion */ 17 | public function write( 18 | string $path, 19 | string $direction, 20 | array $queriesByVersion, 21 | ): bool; 22 | } 23 | -------------------------------------------------------------------------------- /src/Rollup.php: -------------------------------------------------------------------------------- 1 | migrationRepository->getMigrations(); 32 | 33 | if (count($versions) === 0) { 34 | throw RollupFailed::noMigrationsFound(); 35 | } 36 | 37 | if (count($versions) > 1) { 38 | throw RollupFailed::tooManyMigrations(); 39 | } 40 | 41 | $this->metadataStorage->reset(); 42 | 43 | $result = new ExecutionResult($versions->getItems()[0]->getVersion()); 44 | $this->metadataStorage->complete($result); 45 | 46 | return $result->getVersion(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/SchemaDumper.php: -------------------------------------------------------------------------------- 1 | $schemaManager 39 | * @param string[] $excludedTablesRegexes 40 | */ 41 | public function __construct( 42 | private readonly AbstractPlatform $platform, 43 | private readonly AbstractSchemaManager $schemaManager, 44 | private readonly Generator $migrationGenerator, 45 | private readonly SqlGenerator $migrationSqlGenerator, 46 | private readonly array $excludedTablesRegexes = [], 47 | ) { 48 | } 49 | 50 | /** 51 | * @param string[] $excludedTablesRegexes 52 | * 53 | * @throws NoTablesFound 54 | */ 55 | public function dump( 56 | string $fqcn, 57 | array $excludedTablesRegexes = [], 58 | bool $formatted = false, 59 | int $lineLength = 120, 60 | ): string { 61 | $schema = $this->schemaManager->introspectSchema(); 62 | 63 | $up = []; 64 | $down = []; 65 | 66 | foreach ($schema->getTables() as $table) { 67 | if ($this->shouldSkipTable($table, $excludedTablesRegexes)) { 68 | continue; 69 | } 70 | 71 | $upSql = $this->platform->getCreateTableSQL($table); 72 | 73 | $upCode = $this->migrationSqlGenerator->generate( 74 | $upSql, 75 | $formatted, 76 | $lineLength, 77 | ); 78 | 79 | if ($upCode !== '') { 80 | $up[] = $upCode; 81 | } 82 | 83 | $downSql = [$this->platform->getDropTableSQL($table->getQuotedName($this->platform))]; 84 | 85 | $downCode = $this->migrationSqlGenerator->generate( 86 | $downSql, 87 | $formatted, 88 | $lineLength, 89 | ); 90 | 91 | if ($downCode === '') { 92 | continue; 93 | } 94 | 95 | $down[] = $downCode; 96 | } 97 | 98 | if (count($up) === 0) { 99 | throw NoTablesFound::new(); 100 | } 101 | 102 | $up = implode("\n", $up); 103 | $down = implode("\n", $down); 104 | 105 | return $this->migrationGenerator->generateMigration( 106 | $fqcn, 107 | $up, 108 | $down, 109 | ); 110 | } 111 | 112 | /** @param string[] $excludedTablesRegexes */ 113 | private function shouldSkipTable(Table $table, array $excludedTablesRegexes): bool 114 | { 115 | foreach (array_merge($excludedTablesRegexes, $this->excludedTablesRegexes) as $regex) { 116 | if (self::pregMatch($regex, $table->getName()) !== 0) { 117 | return true; 118 | } 119 | } 120 | 121 | return false; 122 | } 123 | 124 | /** 125 | * A local wrapper for "preg_match" which will throw a InvalidArgumentException if there 126 | * is an internal error in the PCRE engine. 127 | * Copied from https://github.com/symfony/symfony/blob/62216ea67762b18982ca3db73c391b0748a49d49/src/Symfony/Component/Yaml/Parser.php#L1072-L1090 128 | * 129 | * @internal 130 | * 131 | * @param mixed[] $matches 132 | * @param int-mask-of $flags 133 | */ 134 | private static function pregMatch(string $pattern, string $subject, array|null &$matches = null, int $flags = 0, int $offset = 0): int 135 | { 136 | $errorMessages = []; 137 | set_error_handler(static function (int $severity, string $message) use (&$errorMessages): bool { 138 | $errorMessages[] = $message; 139 | 140 | return true; 141 | }); 142 | 143 | try { 144 | $ret = preg_match($pattern, $subject, $matches, $flags, $offset); 145 | } finally { 146 | restore_error_handler(); 147 | } 148 | 149 | if ($ret === false) { 150 | throw new InvalidArgumentException(match (preg_last_error()) { 151 | PREG_INTERNAL_ERROR => sprintf('Internal PCRE error, please check your Regex. Reported errors: %s.', implode(', ', $errorMessages)), 152 | default => preg_last_error_msg(), 153 | }); 154 | } 155 | 156 | return $ret; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Tools/BooleanStringFormatter.php: -------------------------------------------------------------------------------- 1 | true, 23 | 'false', '0' => false, 24 | default => $default, 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Tools/BytesFormatter.php: -------------------------------------------------------------------------------- 1 | setAliases(['current']) 27 | ->setDescription('Outputs the current version'); 28 | 29 | parent::configure(); 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output): int 33 | { 34 | $aliasResolver = $this->getDependencyFactory()->getVersionAliasResolver(); 35 | 36 | $version = $aliasResolver->resolveVersionAlias('current'); 37 | if ((string) $version === '0') { 38 | $description = '(No migration executed yet)'; 39 | } else { 40 | try { 41 | $availableMigration = $this->getDependencyFactory()->getMigrationRepository()->getMigration($version); 42 | $description = $availableMigration->getMigration()->getDescription(); 43 | } catch (MigrationClassNotFound) { 44 | $description = '(Migration info not available)'; 45 | } 46 | } 47 | 48 | $this->io->text(sprintf( 49 | "%s%s\n", 50 | (string) $version, 51 | $description !== '' ? ' - ' . $description : '', 52 | )); 53 | 54 | return 0; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Tools/Console/Command/DumpSchemaCommand.php: -------------------------------------------------------------------------------- 1 | setAliases(['dump-schema']) 38 | ->setDescription('Dump the schema for your database to a migration.') 39 | ->setHelp(<<<'EOT' 40 | The %command.name% command dumps the schema for your database to a migration: 41 | 42 | %command.full_name% 43 | 44 | After dumping your schema to a migration, you can rollup your migrations using the migrations:rollup command. 45 | EOT) 46 | ->addOption( 47 | 'formatted', 48 | null, 49 | InputOption::VALUE_NONE, 50 | 'Format the generated SQL.', 51 | ) 52 | ->addOption( 53 | 'namespace', 54 | null, 55 | InputOption::VALUE_REQUIRED, 56 | 'Namespace to use for the generated migrations (defaults to the first namespace definition).', 57 | ) 58 | ->addOption( 59 | 'filter-tables', 60 | null, 61 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 62 | 'Filter the tables to dump via Regex.', 63 | ) 64 | ->addOption( 65 | 'line-length', 66 | null, 67 | InputOption::VALUE_OPTIONAL, 68 | 'Max line length of unformatted lines.', 69 | '120', 70 | ); 71 | } 72 | 73 | /** @throws SchemaDumpRequiresNoMigrations */ 74 | public function execute( 75 | InputInterface $input, 76 | OutputInterface $output, 77 | ): int { 78 | $formatted = $input->getOption('formatted'); 79 | $lineLength = (int) $input->getOption('line-length'); 80 | 81 | $schemaDumper = $this->getDependencyFactory()->getSchemaDumper(); 82 | 83 | if ($formatted) { 84 | if (! class_exists(SqlFormatter::class)) { 85 | throw InvalidOptionUsage::new( 86 | 'The "--formatted" option can only be used if the sql formatter is installed. Please run "composer require doctrine/sql-formatter".', 87 | ); 88 | } 89 | } 90 | 91 | $namespace = $this->getNamespace($input, $output); 92 | 93 | $this->checkNoPreviousDumpExistsForNamespace($namespace); 94 | 95 | $fqcn = $this->getDependencyFactory()->getClassNameGenerator()->generateClassName($namespace); 96 | 97 | $path = $schemaDumper->dump( 98 | $fqcn, 99 | $input->getOption('filter-tables'), 100 | $formatted, 101 | $lineLength, 102 | ); 103 | 104 | $this->io->text([ 105 | sprintf('Dumped your schema to a new migration class at "%s"', $path), 106 | '', 107 | sprintf( 108 | 'To run just this migration for testing purposes, you can use migrations:execute --up "%s"', 109 | addslashes($fqcn), 110 | ), 111 | '', 112 | sprintf( 113 | 'To revert the migration you can use migrations:execute --down "%s"', 114 | addslashes($fqcn), 115 | ), 116 | '', 117 | 'To use this as a rollup migration you can use the migrations:rollup command.', 118 | '', 119 | ]); 120 | 121 | return 0; 122 | } 123 | 124 | private function checkNoPreviousDumpExistsForNamespace(string $namespace): void 125 | { 126 | $migrations = $this->getDependencyFactory()->getMigrationRepository()->getMigrations(); 127 | foreach ($migrations->getItems() as $migration) { 128 | if (str_contains((string) $migration->getVersion(), $namespace)) { 129 | throw SchemaDumpRequiresNoMigrations::new($namespace); 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Tools/Console/Command/GenerateCommand.php: -------------------------------------------------------------------------------- 1 | setAliases(['generate']) 27 | ->setDescription('Generate a blank migration class.') 28 | ->addOption( 29 | 'namespace', 30 | null, 31 | InputOption::VALUE_REQUIRED, 32 | 'The namespace to use for the migration (must be in the list of configured namespaces)', 33 | ) 34 | ->setHelp(<<<'EOT' 35 | The %command.name% command generates a blank migration class: 36 | 37 | %command.full_name% 38 | 39 | EOT); 40 | 41 | parent::configure(); 42 | } 43 | 44 | protected function execute(InputInterface $input, OutputInterface $output): int 45 | { 46 | $migrationGenerator = $this->getDependencyFactory()->getMigrationGenerator(); 47 | 48 | $namespace = $this->getNamespace($input, $output); 49 | 50 | $fqcn = $this->getDependencyFactory()->getClassNameGenerator()->generateClassName($namespace); 51 | 52 | $path = $migrationGenerator->generateMigration($fqcn); 53 | 54 | $this->io->text([ 55 | sprintf('Generated new migration class to "%s"', $path), 56 | '', 57 | sprintf( 58 | 'To run just this migration for testing purposes, you can use migrations:execute --up \'%s\'', 59 | $fqcn, 60 | ), 61 | '', 62 | sprintf( 63 | 'To revert the migration you can use migrations:execute --down \'%s\'', 64 | $fqcn, 65 | ), 66 | '', 67 | ]); 68 | 69 | return 0; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Tools/Console/Command/LatestCommand.php: -------------------------------------------------------------------------------- 1 | setAliases(['latest']) 27 | ->setDescription('Outputs the latest version'); 28 | 29 | parent::configure(); 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output): int 33 | { 34 | $aliasResolver = $this->getDependencyFactory()->getVersionAliasResolver(); 35 | 36 | try { 37 | $version = $aliasResolver->resolveVersionAlias('latest'); 38 | $availableMigration = $this->getDependencyFactory()->getMigrationRepository()->getMigration($version); 39 | $description = $availableMigration->getMigration()->getDescription(); 40 | } catch (NoMigrationsToExecute) { 41 | $version = '0'; 42 | $description = ''; 43 | } 44 | 45 | $this->io->text(sprintf( 46 | "%s%s\n", 47 | $version, 48 | $description !== '' ? ' - ' . $description : '', 49 | )); 50 | 51 | return 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Tools/Console/Command/ListCommand.php: -------------------------------------------------------------------------------- 1 | setAliases(['list-migrations']) 34 | ->setDescription('Display a list of all available migrations and their status.') 35 | ->setHelp(<<<'EOT' 36 | The %command.name% command outputs a list of all available migrations and their status: 37 | 38 | %command.full_name% 39 | EOT); 40 | 41 | parent::configure(); 42 | } 43 | 44 | protected function execute(InputInterface $input, OutputInterface $output): int 45 | { 46 | $versions = $this->getSortedVersions( 47 | $this->getDependencyFactory()->getMigrationPlanCalculator()->getMigrations(), // available migrations 48 | $this->getDependencyFactory()->getMetadataStorage()->getExecutedMigrations(), // executed migrations 49 | ); 50 | 51 | $this->getDependencyFactory()->getMigrationStatusInfosHelper()->listVersions($versions, $output); 52 | 53 | return 0; 54 | } 55 | 56 | /** @return Version[] */ 57 | private function getSortedVersions(AvailableMigrationsList $availableMigrations, ExecutedMigrationsList $executedMigrations): array 58 | { 59 | $availableVersions = array_map(static fn (AvailableMigration $availableMigration): Version => $availableMigration->getVersion(), $availableMigrations->getItems()); 60 | 61 | $executedVersions = array_map(static fn (ExecutedMigration $executedMigration): Version => $executedMigration->getVersion(), $executedMigrations->getItems()); 62 | 63 | $versions = array_unique(array_merge($availableVersions, $executedVersions)); 64 | 65 | $comparator = $this->getDependencyFactory()->getVersionComparator(); 66 | uasort($versions, $comparator->compare(...)); 67 | 68 | return $versions; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Tools/Console/Command/RollupCommand.php: -------------------------------------------------------------------------------- 1 | setAliases(['rollup']) 29 | ->setDescription('Rollup migrations by deleting all tracked versions and insert the one version that exists.') 30 | ->setHelp(<<<'EOT' 31 | The %command.name% command rolls up migrations by deleting all tracked versions and 32 | inserts the one version that exists that was created with the migrations:dump-schema command. 33 | 34 | %command.full_name% 35 | 36 | To dump your schema to a migration version you can use the migrations:dump-schema command. 37 | EOT); 38 | } 39 | 40 | protected function execute(InputInterface $input, OutputInterface $output): int 41 | { 42 | $question = sprintf( 43 | 'WARNING! You are about to execute a migration in database "%s" that could result in schema changes and data loss. Are you sure you wish to continue?', 44 | $this->getDependencyFactory()->getConnection()->getDatabase() ?? '', 45 | ); 46 | 47 | if (! $this->canExecute($question, $input)) { 48 | $this->io->error('Migration cancelled!'); 49 | 50 | return 3; 51 | } 52 | 53 | $this->getDependencyFactory()->getMetadataStorage()->ensureInitialized(); 54 | $version = $this->getDependencyFactory()->getRollup()->rollup(); 55 | 56 | $this->io->success(sprintf( 57 | 'Rolled up migrations to version %s', 58 | (string) $version, 59 | )); 60 | 61 | return 0; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Tools/Console/Command/StatusCommand.php: -------------------------------------------------------------------------------- 1 | setAliases(['status']) 25 | ->setDescription('View the status of a set of migrations.') 26 | ->setHelp(<<<'EOT' 27 | The %command.name% command outputs the status of a set of migrations: 28 | 29 | %command.full_name% 30 | EOT); 31 | 32 | parent::configure(); 33 | } 34 | 35 | protected function execute(InputInterface $input, OutputInterface $output): int 36 | { 37 | $infosHelper = $this->getDependencyFactory()->getMigrationStatusInfosHelper(); 38 | $infosHelper->showMigrationsInfo($output); 39 | 40 | return 0; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Tools/Console/Command/SyncMetadataCommand.php: -------------------------------------------------------------------------------- 1 | setAliases(['sync-metadata-storage']) 23 | ->setDescription('Ensures that the metadata storage is at the latest version.') 24 | ->setHelp(<<<'EOT' 25 | The way metadata is stored in the database can change between releases. 26 | The %command.name% command updates metadata storage to the latest version, 27 | ensuring it is ready to receive migrations generated by the current version of Doctrine Migrations. 28 | 29 | 30 | %command.full_name% 31 | EOT); 32 | } 33 | 34 | public function execute( 35 | InputInterface $input, 36 | OutputInterface $output, 37 | ): int { 38 | $this->getDependencyFactory()->getMetadataStorage()->ensureInitialized(); 39 | 40 | $this->io->success('Metadata storage synchronized'); 41 | 42 | return 0; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Tools/Console/Command/UpToDateCommand.php: -------------------------------------------------------------------------------- 1 | setAliases(['up-to-date']) 38 | ->setDescription('Tells you if your schema is up-to-date.') 39 | ->addOption('fail-on-unregistered', 'u', InputOption::VALUE_NONE, 'Whether to fail when there are unregistered extra migrations found') 40 | ->addOption('list-migrations', 'l', InputOption::VALUE_NONE, 'Show a list of missing or not migrated versions.') 41 | ->setHelp(<<<'EOT' 42 | The %command.name% command tells you if your schema is up-to-date: 43 | 44 | %command.full_name% 45 | EOT); 46 | 47 | parent::configure(); 48 | } 49 | 50 | protected function execute(InputInterface $input, OutputInterface $output): int 51 | { 52 | $statusCalculator = $this->getDependencyFactory()->getMigrationStatusCalculator(); 53 | 54 | $executedUnavailableMigrations = $statusCalculator->getExecutedUnavailableMigrations(); 55 | $newMigrations = $statusCalculator->getNewMigrations(); 56 | $newMigrationsCount = count($newMigrations); 57 | $executedUnavailableMigrationsCount = count($executedUnavailableMigrations); 58 | 59 | if ($newMigrationsCount === 0 && $executedUnavailableMigrationsCount === 0) { 60 | $this->io->success('Up-to-date! No migrations to execute.'); 61 | 62 | return 0; 63 | } 64 | 65 | $exitCode = 0; 66 | if ($newMigrationsCount > 0) { 67 | $this->io->error(sprintf( 68 | 'Out-of-date! %u migration%s available to execute.', 69 | $newMigrationsCount, 70 | $newMigrationsCount > 1 ? 's are' : ' is', 71 | )); 72 | $exitCode = 1; 73 | } 74 | 75 | if ($executedUnavailableMigrationsCount > 0) { 76 | $this->io->error(sprintf( 77 | 'You have %1$u previously executed migration%3$s in the database that %2$s registered migration%3$s.', 78 | $executedUnavailableMigrationsCount, 79 | $executedUnavailableMigrationsCount > 1 ? 'are not' : 'is not a', 80 | $executedUnavailableMigrationsCount > 1 ? 's' : '', 81 | )); 82 | if ($input->getOption('fail-on-unregistered')) { 83 | $exitCode = 2; 84 | } 85 | } 86 | 87 | if ($input->getOption('list-migrations')) { 88 | $versions = $this->getSortedVersions($newMigrations, $executedUnavailableMigrations); 89 | $this->getDependencyFactory()->getMigrationStatusInfosHelper()->listVersions($versions, $output); 90 | 91 | $this->io->newLine(); 92 | } 93 | 94 | return $exitCode; 95 | } 96 | 97 | /** @return Version[] */ 98 | private function getSortedVersions(AvailableMigrationsList $newMigrations, ExecutedMigrationsList $executedUnavailableMigrations): array 99 | { 100 | $executedUnavailableVersion = array_map(static fn (ExecutedMigration $executedMigration): Version => $executedMigration->getVersion(), $executedUnavailableMigrations->getItems()); 101 | 102 | $newVersions = array_map(static fn (AvailableMigration $availableMigration): Version => $availableMigration->getVersion(), $newMigrations->getItems()); 103 | 104 | $versions = array_unique(array_merge($executedUnavailableVersion, $newVersions)); 105 | 106 | $comparator = $this->getDependencyFactory()->getVersionComparator(); 107 | uasort($versions, $comparator->compare(...)); 108 | 109 | return $versions; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Tools/Console/ConsoleInputMigratorConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | hasOption('query-time') ? (bool) $input->getOption('query-time') : false; 23 | $dryRun = $input->hasOption('dry-run') ? (bool) $input->getOption('dry-run') : false; 24 | $allOrNothing = $this->determineAllOrNothingValueFrom($input) ?? $this->configuration->isAllOrNothing(); 25 | 26 | return (new MigratorConfiguration()) 27 | ->setDryRun($dryRun) 28 | ->setTimeAllQueries($timeAllQueries) 29 | ->setAllOrNothing($allOrNothing); 30 | } 31 | 32 | private function determineAllOrNothingValueFrom(InputInterface $input): bool|null 33 | { 34 | $enableAllOrNothingOption = self::ABSENT_CONFIG_VALUE; 35 | $disableAllOrNothingOption = null; 36 | 37 | if ($input->hasOption('no-all-or-nothing')) { 38 | $disableAllOrNothingOption = $input->getOption('no-all-or-nothing'); 39 | } 40 | 41 | $wasOptionExplicitlyPassed = $input->hasOption('all-or-nothing'); 42 | 43 | if ($wasOptionExplicitlyPassed) { 44 | /** 45 | * Due to this option being able to receive optional values, its behavior is tricky: 46 | * - when `--all-or-nothing` option is not provided, the default is set to self::ABSENT_CONFIG_VALUE 47 | * - when `--all-or-nothing` option is provided without values, this will be `null` 48 | * - when `--all-or-nothing` option is provided with a value, we get the provided value 49 | */ 50 | $enableAllOrNothingOption = $input->getOption('all-or-nothing'); 51 | } 52 | 53 | $enableAllOrNothingDeprecation = match ($enableAllOrNothingOption) { 54 | self::ABSENT_CONFIG_VALUE, null => false, 55 | default => true, 56 | }; 57 | 58 | if ($enableAllOrNothingOption !== self::ABSENT_CONFIG_VALUE && $disableAllOrNothingOption === true) { 59 | throw InvalidAllOrNothingConfiguration::new(); 60 | } 61 | 62 | if ($disableAllOrNothingOption === true) { 63 | return false; 64 | } 65 | 66 | if ($enableAllOrNothingDeprecation) { 67 | Deprecation::trigger( 68 | 'doctrine/migrations', 69 | 'https://github.com/doctrine/migrations/issues/1304', 70 | <<<'DEPRECATION' 71 | Context: Passing values to option `--all-or-nothing` 72 | Problem: Passing values is deprecated 73 | Solution: If you need to disable the behavior, use --no-all-or-nothing, 74 | otherwise, pass the option without a value 75 | DEPRECATION, 76 | ); 77 | } 78 | 79 | return match ($enableAllOrNothingOption) { 80 | self::ABSENT_CONFIG_VALUE => null, 81 | null => true, 82 | default => (bool) $enableAllOrNothingOption, 83 | }; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Tools/Console/Exception/ConsoleException.php: -------------------------------------------------------------------------------- 1 | areMigrationsOrganizedByYear()) { 34 | $dir .= $this->appendDir(date('Y')); 35 | } 36 | 37 | if ($configuration->areMigrationsOrganizedByYearAndMonth()) { 38 | $dir .= $this->appendDir(date('m')); 39 | } 40 | 41 | $this->createDirIfNotExists($dir); 42 | 43 | return $dir; 44 | } 45 | 46 | private function appendDir(string $dir): string 47 | { 48 | return DIRECTORY_SEPARATOR . $dir; 49 | } 50 | 51 | private function createDirIfNotExists(string $dir): void 52 | { 53 | if (file_exists($dir)) { 54 | return; 55 | } 56 | 57 | mkdir($dir, 0755, true); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Tools/Console/InvalidAllOrNothingConfiguration.php: -------------------------------------------------------------------------------- 1 | commit(); 36 | } 37 | 38 | public static function rollbackIfInTransaction(Connection $connection): void 39 | { 40 | if (! self::inTransaction($connection)) { 41 | Deprecation::trigger( 42 | 'doctrine/migrations', 43 | 'https://github.com/doctrine/migrations/issues/1169', 44 | <<<'DEPRECATION' 45 | Context: trying to rollback a transaction 46 | Problem: the transaction is already rolled back, relying on silencing is deprecated. 47 | Solution: override `AbstractMigration::isTransactional()` so that it returns false. 48 | Automate that by setting `transactional` to false in the configuration. 49 | More details at https://www.doctrine-project.org/projects/doctrine-migrations/en/stable/explanation/implicit-commits.html 50 | DEPRECATION, 51 | ); 52 | 53 | return; 54 | } 55 | 56 | $connection->rollBack(); 57 | } 58 | 59 | private static function inTransaction(Connection $connection): bool 60 | { 61 | $innermostConnection = self::getInnerConnection($connection); 62 | 63 | /* Attempt to commit or rollback while no transaction is running 64 | results in an exception since PHP 8 + pdo_mysql combination */ 65 | return ! $innermostConnection instanceof PDO || $innermostConnection->inTransaction(); 66 | } 67 | 68 | /** @return object|resource|null */ 69 | private static function getInnerConnection(Connection $connection) 70 | { 71 | try { 72 | return $connection->getNativeConnection(); 73 | } catch (LogicException) { 74 | } 75 | 76 | $innermostConnection = $connection; 77 | while (method_exists($innermostConnection, 'getWrappedConnection')) { 78 | $innermostConnection = $innermostConnection->getWrappedConnection(); 79 | } 80 | 81 | return $innermostConnection; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Version/AliasResolver.php: -------------------------------------------------------------------------------- 1 | metadataStorage->getExecutedMigrations(); 26 | $availableMigration = $this->migrationPlanCalculator->getMigrations(); 27 | 28 | return $executedMigrations->unavailableSubset($availableMigration); 29 | } 30 | 31 | public function getNewMigrations(): AvailableMigrationsList 32 | { 33 | $executedMigrations = $this->metadataStorage->getExecutedMigrations(); 34 | $availableMigration = $this->migrationPlanCalculator->getMigrations(); 35 | 36 | return $availableMigration->newSubset($executedMigrations); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Version/DbalMigrationFactory.php: -------------------------------------------------------------------------------- 1 | connection, 28 | $this->logger, 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Version/DefaultAliasResolver.php: -------------------------------------------------------------------------------- 1 | migrationPlanCalculator->getMigrations(); 54 | $executedMigrations = $this->metadataStorage->getExecutedMigrations(); 55 | 56 | switch ($alias) { 57 | case self::ALIAS_FIRST: 58 | case '0': 59 | return new Version('0'); 60 | 61 | case self::ALIAS_CURRENT: 62 | try { 63 | return $executedMigrations->getLast()->getVersion(); 64 | } catch (NoMigrationsFoundWithCriteria) { 65 | return new Version('0'); 66 | } 67 | 68 | // no break because of return 69 | case self::ALIAS_PREV: 70 | try { 71 | return $executedMigrations->getLast(-1)->getVersion(); 72 | } catch (NoMigrationsFoundWithCriteria) { 73 | return new Version('0'); 74 | } 75 | 76 | // no break because of return 77 | case self::ALIAS_NEXT: 78 | $newMigrations = $this->migrationStatusCalculator->getNewMigrations(); 79 | 80 | try { 81 | return $newMigrations->getFirst()->getVersion(); 82 | } catch (NoMigrationsFoundWithCriteria $e) { 83 | throw NoMigrationsToExecute::new($e); 84 | } 85 | 86 | // no break because of return 87 | case self::ALIAS_LATEST: 88 | try { 89 | return $availableMigrations->getLast()->getVersion(); 90 | } catch (NoMigrationsFoundWithCriteria) { 91 | return $this->resolveVersionAlias(self::ALIAS_CURRENT); 92 | } 93 | 94 | // no break because of return 95 | default: 96 | if ($availableMigrations->hasMigration(new Version($alias))) { 97 | return $availableMigrations->getMigration(new Version($alias))->getVersion(); 98 | } 99 | 100 | if (substr($alias, 0, 7) === self::ALIAS_CURRENT) { 101 | $val = (int) substr($alias, 7); 102 | $targetMigration = null; 103 | if ($val > 0) { 104 | $newMigrations = $this->migrationStatusCalculator->getNewMigrations(); 105 | 106 | return $newMigrations->getFirst($val - 1)->getVersion(); 107 | } 108 | 109 | return $executedMigrations->getLast($val)->getVersion(); 110 | } 111 | } 112 | 113 | throw UnknownMigrationVersion::new($alias); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Version/Direction.php: -------------------------------------------------------------------------------- 1 | direction; 52 | } 53 | 54 | public function getExecutedAt(): DateTimeImmutable|null 55 | { 56 | return $this->executedAt; 57 | } 58 | 59 | public function setExecutedAt(DateTimeImmutable $executedAt): void 60 | { 61 | $this->executedAt = $executedAt; 62 | } 63 | 64 | public function getVersion(): Version 65 | { 66 | return $this->version; 67 | } 68 | 69 | public function hasSql(): bool 70 | { 71 | return count($this->sql) !== 0; 72 | } 73 | 74 | /** @return Query[] */ 75 | public function getSql(): array 76 | { 77 | return $this->sql; 78 | } 79 | 80 | /** @param Query[] $sql */ 81 | public function setSql(array $sql): void 82 | { 83 | $this->sql = $sql; 84 | } 85 | 86 | public function getTime(): float|null 87 | { 88 | return $this->time; 89 | } 90 | 91 | public function setTime(float $time): void 92 | { 93 | $this->time = $time; 94 | } 95 | 96 | public function getMemory(): float|null 97 | { 98 | return $this->memory; 99 | } 100 | 101 | public function setMemory(float $memory): void 102 | { 103 | $this->memory = $memory; 104 | } 105 | 106 | public function setSkipped(bool $skipped): void 107 | { 108 | $this->skipped = $skipped; 109 | } 110 | 111 | public function isSkipped(): bool 112 | { 113 | return $this->skipped; 114 | } 115 | 116 | public function setError(bool $error, Throwable|null $exception = null): void 117 | { 118 | $this->error = $error; 119 | $this->exception = $exception; 120 | } 121 | 122 | public function hasError(): bool 123 | { 124 | return $this->error; 125 | } 126 | 127 | public function getException(): Throwable|null 128 | { 129 | return $this->exception; 130 | } 131 | 132 | public function setToSchema(Schema $toSchema): void 133 | { 134 | $this->toSchema = $toSchema; 135 | } 136 | 137 | public function getToSchema(): Schema 138 | { 139 | if ($this->toSchema === null) { 140 | throw new RuntimeException('Cannot call getToSchema() when toSchema is null.'); 141 | } 142 | 143 | return $this->toSchema; 144 | } 145 | 146 | public function getState(): int 147 | { 148 | return $this->state; 149 | } 150 | 151 | public function setState(int $state): void 152 | { 153 | $this->state = $state; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Version/Executor.php: -------------------------------------------------------------------------------- 1 | arrangeMigrationsForDirection($direction, $this->getMigrations()); 45 | $availableMigrations = array_filter( 46 | $migrationsToCheck, 47 | // in_array third parameter is intentionally false to force object to string casting 48 | static fn (AvailableMigration $availableMigration): bool => in_array($availableMigration->getVersion(), $versions, false), 49 | ); 50 | 51 | $planItems = array_map(static fn (AvailableMigration $availableMigration): MigrationPlan => new MigrationPlan($availableMigration->getVersion(), $availableMigration->getMigration(), $direction), $availableMigrations); 52 | 53 | if (count($planItems) !== count($versions)) { 54 | $plannedVersions = array_map(static fn (MigrationPlan $migrationPlan): Version => $migrationPlan->getVersion(), $planItems); 55 | $diff = array_diff($versions, $plannedVersions); 56 | 57 | throw MigrationClassNotFound::new((string) reset($diff)); 58 | } 59 | 60 | return new MigrationPlanList($planItems, $direction); 61 | } 62 | 63 | public function getPlanUntilVersion(Version $to): MigrationPlanList 64 | { 65 | if ((string) $to !== '0' && ! $this->migrationRepository->hasMigration((string) $to)) { 66 | throw MigrationClassNotFound::new((string) $to); 67 | } 68 | 69 | $availableMigrations = $this->getMigrations(); // migrations are sorted at this point 70 | $executedMigrations = $this->metadataStorage->getExecutedMigrations(); 71 | 72 | $direction = $this->findDirection($to, $executedMigrations, $availableMigrations); 73 | 74 | $migrationsToCheck = $this->arrangeMigrationsForDirection($direction, $availableMigrations); 75 | 76 | $toExecute = $this->findMigrationsToExecute($to, $migrationsToCheck, $direction, $executedMigrations); 77 | 78 | return new MigrationPlanList(array_map(static fn (AvailableMigration $migration): MigrationPlan => new MigrationPlan($migration->getVersion(), $migration->getMigration(), $direction), $toExecute), $direction); 79 | } 80 | 81 | public function getMigrations(): AvailableMigrationsList 82 | { 83 | $availableMigrations = $this->migrationRepository->getMigrations()->getItems(); 84 | uasort($availableMigrations, fn (AvailableMigration $a, AvailableMigration $b): int => $this->sorter->compare($a->getVersion(), $b->getVersion())); 85 | 86 | return new AvailableMigrationsList($availableMigrations); 87 | } 88 | 89 | private function findDirection(Version $to, ExecutedMigrationsList $executedMigrations, AvailableMigrationsList $availableMigrations): string 90 | { 91 | if ((string) $to === '0') { 92 | return Direction::DOWN; 93 | } 94 | 95 | foreach ($availableMigrations->getItems() as $availableMigration) { 96 | if ($availableMigration->getVersion()->equals($to)) { 97 | break; 98 | } 99 | 100 | if (! $executedMigrations->hasMigration($availableMigration->getVersion())) { 101 | return Direction::UP; 102 | } 103 | } 104 | 105 | if ($executedMigrations->hasMigration($to) && ! $executedMigrations->getLast()->getVersion()->equals($to)) { 106 | return Direction::DOWN; 107 | } 108 | 109 | return Direction::UP; 110 | } 111 | 112 | /** @return AvailableMigration[] */ 113 | private function arrangeMigrationsForDirection(string $direction, Metadata\AvailableMigrationsList $availableMigrations): array 114 | { 115 | return $direction === Direction::UP ? $availableMigrations->getItems() : array_reverse($availableMigrations->getItems()); 116 | } 117 | 118 | /** 119 | * @param AvailableMigration[] $migrationsToCheck 120 | * 121 | * @return AvailableMigration[] 122 | */ 123 | private function findMigrationsToExecute(Version $to, array $migrationsToCheck, string $direction, ExecutedMigrationsList $executedMigrations): array 124 | { 125 | $toExecute = []; 126 | foreach ($migrationsToCheck as $availableMigration) { 127 | if ($direction === Direction::DOWN && $availableMigration->getVersion()->equals($to)) { 128 | break; 129 | } 130 | 131 | if ($direction === Direction::UP && ! $executedMigrations->hasMigration($availableMigration->getVersion())) { 132 | $toExecute[] = $availableMigration; 133 | } elseif ($direction === Direction::DOWN && $executedMigrations->hasMigration($availableMigration->getVersion())) { 134 | $toExecute[] = $availableMigration; 135 | } 136 | 137 | if ($direction === Direction::UP && $availableMigration->getVersion()->equals($to)) { 138 | break; 139 | } 140 | } 141 | 142 | return $toExecute; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Version/State.php: -------------------------------------------------------------------------------- 1 | version; 18 | } 19 | 20 | public function equals(mixed $object): bool 21 | { 22 | return $object instanceof self && $object->version === $this->version; 23 | } 24 | } 25 | --------------------------------------------------------------------------------