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

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($row, CASE_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 $a, ExecutedMigration $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 $availableMigration, ExecutedMigration $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. }