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

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($row, CASE_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 $a, ExecutedMigration $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 $availableMigration, ExecutedMigration $executedMigration): bool
  225. {
  226. return (string) $availableMigration->getVersion() === (string) $executedMigration->getVersion()
  227. || strpos(
  228. (string) $availableMigration->getVersion(),
  229. (string) $executedMigration->getVersion(),
  230. ) !== strlen((string) $availableMigration->getVersion()) -
  231. strlen((string) $executedMigration->getVersion());
  232. }
  233. }