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

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