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

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