welcome back to dyb-tech

This commit is contained in:
Daniel Guzman
2024-05-18 02:28:01 +02:00
parent 9513cdba09
commit 9f30bc98c7
6149 changed files with 668407 additions and 0 deletions
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use Doctrine\Migrations\AbstractMigration;
use Doctrine\Migrations\Version\Version;
/**
* Available migrations may or may not be already executed
* The migration might be already executed or not.
*/
final class AvailableMigration
{
public function __construct(
private readonly Version $version,
private readonly AbstractMigration $migration,
) {
}
public function getVersion(): Version
{
return $this->version;
}
public function getMigration(): AbstractMigration
{
return $this->migration;
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use Countable;
use Doctrine\Migrations\Exception\MigrationNotAvailable;
use Doctrine\Migrations\Exception\NoMigrationsFoundWithCriteria;
use Doctrine\Migrations\Version\Version;
use function array_filter;
use function array_values;
use function count;
/**
* Represents a sorted list of migrations that may or maybe not be already executed.
*/
final class AvailableMigrationsList implements Countable
{
/** @var AvailableMigration[] */
private array $items = [];
/** @param AvailableMigration[] $items */
public function __construct(array $items)
{
$this->items = array_values($items);
}
/** @return AvailableMigration[] */
public function getItems(): array
{
return $this->items;
}
public function getFirst(int $offset = 0): AvailableMigration
{
if (! isset($this->items[$offset])) {
throw NoMigrationsFoundWithCriteria::new('first' . ($offset > 0 ? '+' . $offset : ''));
}
return $this->items[$offset];
}
public function getLast(int $offset = 0): AvailableMigration
{
$offset = count($this->items) - 1 - (-1 * $offset);
if (! isset($this->items[$offset])) {
throw NoMigrationsFoundWithCriteria::new('last' . ($offset > 0 ? '+' . $offset : ''));
}
return $this->items[$offset];
}
public function count(): int
{
return count($this->items);
}
public function hasMigration(Version $version): bool
{
foreach ($this->items as $migration) {
if ($migration->getVersion()->equals($version)) {
return true;
}
}
return false;
}
public function getMigration(Version $version): AvailableMigration
{
foreach ($this->items as $migration) {
if ($migration->getVersion()->equals($version)) {
return $migration;
}
}
throw MigrationNotAvailable::forVersion($version);
}
public function newSubset(ExecutedMigrationsList $executedMigrations): self
{
return new self(array_filter($this->getItems(), static fn (AvailableMigration $migration): bool => ! $executedMigrations->hasMigration($migration->getVersion())));
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use Countable;
use Doctrine\Migrations\Exception\MigrationNotAvailable;
use Doctrine\Migrations\Version\Version;
use function array_values;
use function count;
/**
* Represents a non sorted list of migrations that may or may not be already executed.
*/
final class AvailableMigrationsSet implements Countable
{
/** @var AvailableMigration[] */
private array $items = [];
/** @param AvailableMigration[] $items */
public function __construct(array $items)
{
$this->items = array_values($items);
}
/** @return AvailableMigration[] */
public function getItems(): array
{
return $this->items;
}
public function count(): int
{
return count($this->items);
}
public function hasMigration(Version $version): bool
{
foreach ($this->items as $migration) {
if ($migration->getVersion()->equals($version)) {
return true;
}
}
return false;
}
public function getMigration(Version $version): AvailableMigration
{
foreach ($this->items as $migration) {
if ($migration->getVersion()->equals($version)) {
return $migration;
}
}
throw MigrationNotAvailable::forVersion($version);
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use DateTimeImmutable;
use Doctrine\Migrations\Version\Version;
/**
* Represents an already executed migration.
* The migration might be not available anymore.
*/
final class ExecutedMigration
{
public function __construct(
private readonly Version $version,
private readonly DateTimeImmutable|null $executedAt = null,
public float|null $executionTime = null,
) {
}
public function getExecutionTime(): float|null
{
return $this->executionTime;
}
public function getExecutedAt(): DateTimeImmutable|null
{
return $this->executedAt;
}
public function getVersion(): Version
{
return $this->version;
}
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use Countable;
use Doctrine\Migrations\Exception\MigrationNotExecuted;
use Doctrine\Migrations\Exception\NoMigrationsFoundWithCriteria;
use Doctrine\Migrations\Version\Version;
use function array_filter;
use function array_values;
use function count;
/**
* Represents a sorted list of executed migrations.
* The migrations in this set might be not available anymore.
*/
final class ExecutedMigrationsList implements Countable
{
/** @var ExecutedMigration[] */
private array $items = [];
/** @param ExecutedMigration[] $items */
public function __construct(array $items)
{
$this->items = array_values($items);
}
/** @return ExecutedMigration[] */
public function getItems(): array
{
return $this->items;
}
public function getFirst(int $offset = 0): ExecutedMigration
{
if (! isset($this->items[$offset])) {
throw NoMigrationsFoundWithCriteria::new('first' . ($offset > 0 ? '+' . $offset : ''));
}
return $this->items[$offset];
}
public function getLast(int $offset = 0): ExecutedMigration
{
$offset = count($this->items) - 1 - (-1 * $offset);
if (! isset($this->items[$offset])) {
throw NoMigrationsFoundWithCriteria::new('last' . ($offset > 0 ? '+' . $offset : ''));
}
return $this->items[$offset];
}
public function count(): int
{
return count($this->items);
}
public function hasMigration(Version $version): bool
{
foreach ($this->items as $migration) {
if ($migration->getVersion()->equals($version)) {
return true;
}
}
return false;
}
public function getMigration(Version $version): ExecutedMigration
{
foreach ($this->items as $migration) {
if ($migration->getVersion()->equals($version)) {
return $migration;
}
}
throw MigrationNotExecuted::new((string) $version);
}
public function unavailableSubset(AvailableMigrationsList $availableMigrations): self
{
return new self(array_filter($this->getItems(), static fn (ExecutedMigration $migration): bool => ! $availableMigrations->hasMigration($migration->getVersion())));
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use Doctrine\Migrations\AbstractMigration;
use Doctrine\Migrations\Exception\PlanAlreadyExecuted;
use Doctrine\Migrations\Version\ExecutionResult;
use Doctrine\Migrations\Version\Version;
/**
* Represents an available migration to be executed in a specific direction.
*/
final class MigrationPlan
{
public ExecutionResult|null $result = null;
public function __construct(
private readonly Version $version,
private readonly AbstractMigration $migration,
private readonly string $direction,
) {
}
public function getVersion(): Version
{
return $this->version;
}
public function getResult(): ExecutionResult|null
{
return $this->result;
}
public function markAsExecuted(ExecutionResult $result): void
{
if ($this->result !== null) {
throw PlanAlreadyExecuted::new();
}
$this->result = $result;
}
public function getMigration(): AbstractMigration
{
return $this->migration;
}
public function getDirection(): string
{
return $this->direction;
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata;
use Countable;
use Doctrine\Migrations\Exception\NoMigrationsFoundWithCriteria;
use function count;
use function end;
use function reset;
/**
* Represents a sorted list of MigrationPlan instances to execute.
*/
final class MigrationPlanList implements Countable
{
/** @param MigrationPlan[] $items */
public function __construct(
private array $items,
private readonly string $direction,
) {
}
public function count(): int
{
return count($this->items);
}
/** @return MigrationPlan[] */
public function getItems(): array
{
return $this->items;
}
public function getDirection(): string
{
return $this->direction;
}
public function getFirst(): MigrationPlan
{
if (count($this->items) === 0) {
throw NoMigrationsFoundWithCriteria::new('first');
}
return reset($this->items);
}
public function getLast(): MigrationPlan
{
if (count($this->items) === 0) {
throw NoMigrationsFoundWithCriteria::new('last');
}
return end($this->items);
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata\Storage;
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
use Doctrine\Migrations\Query\Query;
use Doctrine\Migrations\Version\ExecutionResult;
/** @method iterable<Query> getSql(ExecutionResult $result); */
interface MetadataStorage
{
public function ensureInitialized(): void;
public function getExecutedMigrations(): ExecutedMigrationsList;
public function complete(ExecutionResult $result): void;
public function reset(): void;
}
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata\Storage;
interface MetadataStorageConfiguration
{
}
@@ -0,0 +1,285 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata\Storage;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\Exception\MetadataStorageError;
use Doctrine\Migrations\Metadata\AvailableMigration;
use Doctrine\Migrations\Metadata\ExecutedMigration;
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
use Doctrine\Migrations\MigrationsRepository;
use Doctrine\Migrations\Query\Query;
use Doctrine\Migrations\Version\Comparator as MigrationsComparator;
use Doctrine\Migrations\Version\Direction;
use Doctrine\Migrations\Version\ExecutionResult;
use Doctrine\Migrations\Version\Version;
use InvalidArgumentException;
use function array_change_key_case;
use function floatval;
use function round;
use function sprintf;
use function strlen;
use function strpos;
use function strtolower;
use function uasort;
use const CASE_LOWER;
final class TableMetadataStorage implements MetadataStorage
{
private bool $isInitialized = false;
private bool $schemaUpToDate = false;
/** @var AbstractSchemaManager<AbstractPlatform> */
private readonly AbstractSchemaManager $schemaManager;
private readonly AbstractPlatform $platform;
private readonly TableMetadataStorageConfiguration $configuration;
public function __construct(
private readonly Connection $connection,
private readonly MigrationsComparator $comparator,
MetadataStorageConfiguration|null $configuration = null,
private readonly MigrationsRepository|null $migrationRepository = null,
) {
$this->schemaManager = $connection->createSchemaManager();
$this->platform = $connection->getDatabasePlatform();
if ($configuration !== null && ! ($configuration instanceof TableMetadataStorageConfiguration)) {
throw new InvalidArgumentException(sprintf(
'%s accepts only %s as configuration',
self::class,
TableMetadataStorageConfiguration::class,
));
}
$this->configuration = $configuration ?? new TableMetadataStorageConfiguration();
}
public function getExecutedMigrations(): ExecutedMigrationsList
{
if (! $this->isInitialized()) {
return new ExecutedMigrationsList([]);
}
$this->checkInitialization();
$rows = $this->connection->fetchAllAssociative(sprintf('SELECT * FROM %s', $this->configuration->getTableName()));
$migrations = [];
foreach ($rows as $row) {
$row = array_change_key_case($row, CASE_LOWER);
$version = new Version($row[strtolower($this->configuration->getVersionColumnName())]);
$executedAt = $row[strtolower($this->configuration->getExecutedAtColumnName())] ?? '';
$executedAt = $executedAt !== ''
? DateTimeImmutable::createFromFormat($this->platform->getDateTimeFormatString(), $executedAt)
: null;
$executionTime = isset($row[strtolower($this->configuration->getExecutionTimeColumnName())])
? floatval($row[strtolower($this->configuration->getExecutionTimeColumnName())] / 1000)
: null;
$migration = new ExecutedMigration(
$version,
$executedAt instanceof DateTimeImmutable ? $executedAt : null,
$executionTime,
);
$migrations[(string) $version] = $migration;
}
uasort($migrations, fn (ExecutedMigration $a, ExecutedMigration $b): int => $this->comparator->compare($a->getVersion(), $b->getVersion()));
return new ExecutedMigrationsList($migrations);
}
public function reset(): void
{
$this->checkInitialization();
$this->connection->executeStatement(
sprintf(
'DELETE FROM %s WHERE 1 = 1',
$this->platform->quoteIdentifier($this->configuration->getTableName()),
),
);
}
public function complete(ExecutionResult $result): void
{
$this->checkInitialization();
if ($result->getDirection() === Direction::DOWN) {
$this->connection->delete($this->configuration->getTableName(), [
$this->configuration->getVersionColumnName() => (string) $result->getVersion(),
]);
} else {
$this->connection->insert($this->configuration->getTableName(), [
$this->configuration->getVersionColumnName() => (string) $result->getVersion(),
$this->configuration->getExecutedAtColumnName() => $result->getExecutedAt(),
$this->configuration->getExecutionTimeColumnName() => $result->getTime() === null ? null : (int) round($result->getTime() * 1000),
], [
Types::STRING,
Types::DATETIME_IMMUTABLE,
Types::INTEGER,
]);
}
}
/** @return iterable<Query> */
public function getSql(ExecutionResult $result): iterable
{
yield new Query('-- Version ' . (string) $result->getVersion() . ' update table metadata');
if ($result->getDirection() === Direction::DOWN) {
yield new Query(sprintf(
'DELETE FROM %s WHERE %s = %s',
$this->configuration->getTableName(),
$this->configuration->getVersionColumnName(),
$this->connection->quote((string) $result->getVersion()),
));
return;
}
yield new Query(sprintf(
'INSERT INTO %s (%s, %s, %s) VALUES (%s, %s, 0)',
$this->configuration->getTableName(),
$this->configuration->getVersionColumnName(),
$this->configuration->getExecutedAtColumnName(),
$this->configuration->getExecutionTimeColumnName(),
$this->connection->quote((string) $result->getVersion()),
$this->connection->quote(($result->getExecutedAt() ?? new DateTimeImmutable())->format('Y-m-d H:i:s')),
));
}
public function ensureInitialized(): void
{
if (! $this->isInitialized()) {
$expectedSchemaChangelog = $this->getExpectedTable();
$this->schemaManager->createTable($expectedSchemaChangelog);
$this->schemaUpToDate = true;
$this->isInitialized = true;
return;
}
$this->isInitialized = true;
$expectedSchemaChangelog = $this->getExpectedTable();
$diff = $this->needsUpdate($expectedSchemaChangelog);
if ($diff === null) {
$this->schemaUpToDate = true;
return;
}
$this->schemaUpToDate = true;
$this->schemaManager->alterTable($diff);
$this->updateMigratedVersionsFromV1orV2toV3();
}
private function needsUpdate(Table $expectedTable): TableDiff|null
{
if ($this->schemaUpToDate) {
return null;
}
$currentTable = $this->schemaManager->introspectTable($this->configuration->getTableName());
$diff = $this->schemaManager->createComparator()->compareTables($currentTable, $expectedTable);
return $diff->isEmpty() ? null : $diff;
}
private function isInitialized(): bool
{
if ($this->isInitialized) {
return $this->isInitialized;
}
if ($this->connection instanceof PrimaryReadReplicaConnection) {
$this->connection->ensureConnectedToPrimary();
}
return $this->schemaManager->tablesExist([$this->configuration->getTableName()]);
}
private function checkInitialization(): void
{
if (! $this->isInitialized()) {
throw MetadataStorageError::notInitialized();
}
$expectedTable = $this->getExpectedTable();
if ($this->needsUpdate($expectedTable) !== null) {
throw MetadataStorageError::notUpToDate();
}
}
private function getExpectedTable(): Table
{
$schemaChangelog = new Table($this->configuration->getTableName());
$schemaChangelog->addColumn(
$this->configuration->getVersionColumnName(),
'string',
['notnull' => true, 'length' => $this->configuration->getVersionColumnLength()],
);
$schemaChangelog->addColumn($this->configuration->getExecutedAtColumnName(), 'datetime', ['notnull' => false]);
$schemaChangelog->addColumn($this->configuration->getExecutionTimeColumnName(), 'integer', ['notnull' => false]);
$schemaChangelog->setPrimaryKey([$this->configuration->getVersionColumnName()]);
return $schemaChangelog;
}
private function updateMigratedVersionsFromV1orV2toV3(): void
{
if ($this->migrationRepository === null) {
return;
}
$availableMigrations = $this->migrationRepository->getMigrations()->getItems();
$executedMigrations = $this->getExecutedMigrations()->getItems();
foreach ($availableMigrations as $availableMigration) {
foreach ($executedMigrations as $k => $executedMigration) {
if ($this->isAlreadyV3Format($availableMigration, $executedMigration)) {
continue;
}
$this->connection->update(
$this->configuration->getTableName(),
[
$this->configuration->getVersionColumnName() => (string) $availableMigration->getVersion(),
],
[
$this->configuration->getVersionColumnName() => (string) $executedMigration->getVersion(),
],
);
unset($executedMigrations[$k]);
}
}
}
private function isAlreadyV3Format(AvailableMigration $availableMigration, ExecutedMigration $executedMigration): bool
{
return strpos(
(string) $availableMigration->getVersion(),
(string) $executedMigration->getVersion(),
) !== strlen((string) $availableMigration->getVersion()) -
strlen((string) $executedMigration->getVersion());
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Doctrine\Migrations\Metadata\Storage;
final class TableMetadataStorageConfiguration implements MetadataStorageConfiguration
{
private string $tableName = 'doctrine_migration_versions';
private string $versionColumnName = 'version';
private int $versionColumnLength = 191;
private string $executedAtColumnName = 'executed_at';
private string $executionTimeColumnName = 'execution_time';
public function getTableName(): string
{
return $this->tableName;
}
public function setTableName(string $tableName): void
{
$this->tableName = $tableName;
}
public function getVersionColumnName(): string
{
return $this->versionColumnName;
}
public function setVersionColumnName(string $versionColumnName): void
{
$this->versionColumnName = $versionColumnName;
}
public function getVersionColumnLength(): int
{
return $this->versionColumnLength;
}
public function setVersionColumnLength(int $versionColumnLength): void
{
$this->versionColumnLength = $versionColumnLength;
}
public function getExecutedAtColumnName(): string
{
return $this->executedAtColumnName;
}
public function setExecutedAtColumnName(string $executedAtColumnName): void
{
$this->executedAtColumnName = $executedAtColumnName;
}
public function getExecutionTimeColumnName(): string
{
return $this->executionTimeColumnName;
}
public function setExecutionTimeColumnName(string $executionTimeColumnName): void
{
$this->executionTimeColumnName = $executionTimeColumnName;
}
}