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

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