vendor/doctrine/migrations/lib/Doctrine/Migrations/Metadata/Storage/TableMetadataStorage.php line 215

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