vendor/doctrine/migrations/src/Metadata/Storage/TableMetadataStorage.php line 218

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\Migrations\Metadata\Storage;
  4. use DateTimeImmutable;
  5. use Doctrine\DBAL\Connection;
  6. use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
  7. use Doctrine\DBAL\Platforms\AbstractPlatform;
  8. use Doctrine\DBAL\Schema\AbstractSchemaManager;
  9. use Doctrine\DBAL\Schema\Table;
  10. use Doctrine\DBAL\Schema\TableDiff;
  11. use Doctrine\DBAL\Types\Types;
  12. use Doctrine\Migrations\Exception\MetadataStorageError;
  13. use Doctrine\Migrations\Metadata\AvailableMigration;
  14. use Doctrine\Migrations\Metadata\ExecutedMigration;
  15. use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
  16. use Doctrine\Migrations\MigrationsRepository;
  17. use Doctrine\Migrations\Query\Query;
  18. use Doctrine\Migrations\Version\Comparator as MigrationsComparator;
  19. use Doctrine\Migrations\Version\Direction;
  20. use Doctrine\Migrations\Version\ExecutionResult;
  21. use Doctrine\Migrations\Version\Version;
  22. use InvalidArgumentException;
  23. use function array_change_key_case;
  24. use function floatval;
  25. use function round;
  26. use function sprintf;
  27. use function strlen;
  28. use function strpos;
  29. use function strtolower;
  30. use function uasort;
  31. use const CASE_LOWER;
  32. final class TableMetadataStorage implements MetadataStorage
  33. {
  34.     private bool $isInitialized false;
  35.     private bool $schemaUpToDate false;
  36.     /** @var AbstractSchemaManager<AbstractPlatform> */
  37.     private readonly AbstractSchemaManager $schemaManager;
  38.     private readonly AbstractPlatform $platform;
  39.     private readonly TableMetadataStorageConfiguration $configuration;
  40.     public function __construct(
  41.         private readonly Connection $connection,
  42.         private readonly MigrationsComparator $comparator,
  43.         MetadataStorageConfiguration|null $configuration null,
  44.         private readonly MigrationsRepository|null $migrationRepository null,
  45.     ) {
  46.         $this->schemaManager $connection->createSchemaManager();
  47.         $this->platform      $connection->getDatabasePlatform();
  48.         if ($configuration !== null && ! ($configuration instanceof TableMetadataStorageConfiguration)) {
  49.             throw new InvalidArgumentException(sprintf(
  50.                 '%s accepts only %s as configuration',
  51.                 self::class,
  52.                 TableMetadataStorageConfiguration::class,
  53.             ));
  54.         }
  55.         $this->configuration $configuration ?? new TableMetadataStorageConfiguration();
  56.     }
  57.     public function getExecutedMigrations(): ExecutedMigrationsList
  58.     {
  59.         if (! $this->isInitialized()) {
  60.             return new ExecutedMigrationsList([]);
  61.         }
  62.         $this->checkInitialization();
  63.         $rows $this->connection->fetchAllAssociative(sprintf('SELECT * FROM %s'$this->configuration->getTableName()));
  64.         $migrations = [];
  65.         foreach ($rows as $row) {
  66.             $row array_change_key_case($rowCASE_LOWER);
  67.             $version = new Version($row[strtolower($this->configuration->getVersionColumnName())]);
  68.             $executedAt $row[strtolower($this->configuration->getExecutedAtColumnName())] ?? '';
  69.             $executedAt $executedAt !== ''
  70.                 DateTimeImmutable::createFromFormat($this->platform->getDateTimeFormatString(), $executedAt)
  71.                 : null;
  72.             $executionTime = isset($row[strtolower($this->configuration->getExecutionTimeColumnName())])
  73.                 ? floatval($row[strtolower($this->configuration->getExecutionTimeColumnName())] / 1000)
  74.                 : null;
  75.             $migration = new ExecutedMigration(
  76.                 $version,
  77.                 $executedAt instanceof DateTimeImmutable $executedAt null,
  78.                 $executionTime,
  79.             );
  80.             $migrations[(string) $version] = $migration;
  81.         }
  82.         uasort($migrations, fn (ExecutedMigration $aExecutedMigration $b): int => $this->comparator->compare($a->getVersion(), $b->getVersion()));
  83.         return new ExecutedMigrationsList($migrations);
  84.     }
  85.     public function reset(): void
  86.     {
  87.         $this->checkInitialization();
  88.         $this->connection->executeStatement(
  89.             sprintf(
  90.                 'DELETE FROM %s WHERE 1 = 1',
  91.                 $this->platform->quoteIdentifier($this->configuration->getTableName()),
  92.             ),
  93.         );
  94.     }
  95.     public function complete(ExecutionResult $result): void
  96.     {
  97.         $this->checkInitialization();
  98.         if ($result->getDirection() === Direction::DOWN) {
  99.             $this->connection->delete($this->configuration->getTableName(), [
  100.                 $this->configuration->getVersionColumnName() => (string) $result->getVersion(),
  101.             ]);
  102.         } else {
  103.             $this->connection->insert($this->configuration->getTableName(), [
  104.                 $this->configuration->getVersionColumnName() => (string) $result->getVersion(),
  105.                 $this->configuration->getExecutedAtColumnName() => $result->getExecutedAt(),
  106.                 $this->configuration->getExecutionTimeColumnName() => $result->getTime() === null null : (int) round($result->getTime() * 1000),
  107.             ], [
  108.                 Types::STRING,
  109.                 Types::DATETIME_IMMUTABLE,
  110.                 Types::INTEGER,
  111.             ]);
  112.         }
  113.     }
  114.     /** @return iterable<Query> */
  115.     public function getSql(ExecutionResult $result): iterable
  116.     {
  117.         yield new Query('-- Version ' . (string) $result->getVersion() . ' update table metadata');
  118.         if ($result->getDirection() === Direction::DOWN) {
  119.             yield new Query(sprintf(
  120.                 'DELETE FROM %s WHERE %s = %s',
  121.                 $this->configuration->getTableName(),
  122.                 $this->configuration->getVersionColumnName(),
  123.                 $this->connection->quote((string) $result->getVersion()),
  124.             ));
  125.             return;
  126.         }
  127.         yield new Query(sprintf(
  128.             'INSERT INTO %s (%s, %s, %s) VALUES (%s, %s, 0)',
  129.             $this->configuration->getTableName(),
  130.             $this->configuration->getVersionColumnName(),
  131.             $this->configuration->getExecutedAtColumnName(),
  132.             $this->configuration->getExecutionTimeColumnName(),
  133.             $this->connection->quote((string) $result->getVersion()),
  134.             $this->connection->quote(($result->getExecutedAt() ?? new DateTimeImmutable())->format('Y-m-d H:i:s')),
  135.         ));
  136.     }
  137.     public function ensureInitialized(): void
  138.     {
  139.         if (! $this->isInitialized()) {
  140.             $expectedSchemaChangelog $this->getExpectedTable();
  141.             $this->schemaManager->createTable($expectedSchemaChangelog);
  142.             $this->schemaUpToDate true;
  143.             $this->isInitialized  true;
  144.             return;
  145.         }
  146.         $this->isInitialized     true;
  147.         $expectedSchemaChangelog $this->getExpectedTable();
  148.         $diff                    $this->needsUpdate($expectedSchemaChangelog);
  149.         if ($diff === null) {
  150.             $this->schemaUpToDate true;
  151.             return;
  152.         }
  153.         $this->schemaUpToDate true;
  154.         $this->schemaManager->alterTable($diff);
  155.         $this->updateMigratedVersionsFromV1orV2toV3();
  156.     }
  157.     private function needsUpdate(Table $expectedTable): TableDiff|null
  158.     {
  159.         if ($this->schemaUpToDate) {
  160.             return null;
  161.         }
  162.         $currentTable $this->schemaManager->introspectTable($this->configuration->getTableName());
  163.         $diff         $this->schemaManager->createComparator()->compareTables($currentTable$expectedTable);
  164.         return $diff->isEmpty() ? null $diff;
  165.     }
  166.     private function isInitialized(): bool
  167.     {
  168.         if ($this->isInitialized) {
  169.             return $this->isInitialized;
  170.         }
  171.         if ($this->connection instanceof PrimaryReadReplicaConnection) {
  172.             $this->connection->ensureConnectedToPrimary();
  173.         }
  174.         return $this->schemaManager->tablesExist([$this->configuration->getTableName()]);
  175.     }
  176.     private function checkInitialization(): void
  177.     {
  178.         if (! $this->isInitialized()) {
  179.             throw MetadataStorageError::notInitialized();
  180.         }
  181.         $expectedTable $this->getExpectedTable();
  182.         if ($this->needsUpdate($expectedTable) !== null) {
  183.             throw MetadataStorageError::notUpToDate();
  184.         }
  185.     }
  186.     private function getExpectedTable(): Table
  187.     {
  188.         $schemaChangelog = new Table($this->configuration->getTableName());
  189.         $schemaChangelog->addColumn(
  190.             $this->configuration->getVersionColumnName(),
  191.             'string',
  192.             ['notnull' => true'length' => $this->configuration->getVersionColumnLength()],
  193.         );
  194.         $schemaChangelog->addColumn($this->configuration->getExecutedAtColumnName(), 'datetime', ['notnull' => false]);
  195.         $schemaChangelog->addColumn($this->configuration->getExecutionTimeColumnName(), 'integer', ['notnull' => false]);
  196.         $schemaChangelog->setPrimaryKey([$this->configuration->getVersionColumnName()]);
  197.         return $schemaChangelog;
  198.     }
  199.     private function updateMigratedVersionsFromV1orV2toV3(): void
  200.     {
  201.         if ($this->migrationRepository === null) {
  202.             return;
  203.         }
  204.         $availableMigrations $this->migrationRepository->getMigrations()->getItems();
  205.         $executedMigrations  $this->getExecutedMigrations()->getItems();
  206.         foreach ($availableMigrations as $availableMigration) {
  207.             foreach ($executedMigrations as $k => $executedMigration) {
  208.                 if ($this->isAlreadyV3Format($availableMigration$executedMigration)) {
  209.                     continue;
  210.                 }
  211.                 $this->connection->update(
  212.                     $this->configuration->getTableName(),
  213.                     [
  214.                         $this->configuration->getVersionColumnName() => (string) $availableMigration->getVersion(),
  215.                     ],
  216.                     [
  217.                         $this->configuration->getVersionColumnName() => (string) $executedMigration->getVersion(),
  218.                     ],
  219.                 );
  220.                 unset($executedMigrations[$k]);
  221.             }
  222.         }
  223.     }
  224.     private function isAlreadyV3Format(AvailableMigration $availableMigrationExecutedMigration $executedMigration): bool
  225.     {
  226.         return strpos(
  227.             (string) $availableMigration->getVersion(),
  228.             (string) $executedMigration->getVersion(),
  229.         ) !== strlen((string) $availableMigration->getVersion()) -
  230.                 strlen((string) $executedMigration->getVersion());
  231.     }
  232. }