The start of something beautiful
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
/**
|
||||
* The DefaultAliasResolver class is responsible for resolving aliases like first, current, etc. to the actual version number.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface AliasResolver
|
||||
{
|
||||
/**
|
||||
* Returns the version number from an alias.
|
||||
*
|
||||
* Supported aliases are:
|
||||
*
|
||||
* - first: The very first version before any migrations have been run.
|
||||
* - current: The current version.
|
||||
* - prev: The version prior to the current version.
|
||||
* - next: The version following the current version.
|
||||
* - latest: The latest available version.
|
||||
*
|
||||
* If an existing version number is specified, it is returned verbatim.
|
||||
*/
|
||||
public function resolveVersionAlias(string $alias): Version;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
use function strcmp;
|
||||
|
||||
final class AlphabeticalComparator implements Comparator
|
||||
{
|
||||
public function compare(Version $a, Version $b): int
|
||||
{
|
||||
return strcmp((string) $a, (string) $b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
interface Comparator
|
||||
{
|
||||
public function compare(Version $a, Version $b): int;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
use Doctrine\Migrations\Metadata\AvailableMigrationsList;
|
||||
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
|
||||
use Doctrine\Migrations\Metadata\Storage\MetadataStorage;
|
||||
|
||||
/**
|
||||
* The MigrationPlanCalculator is responsible for calculating the plan for migrating from the current
|
||||
* version to another version.
|
||||
*/
|
||||
final class CurrentMigrationStatusCalculator implements MigrationStatusCalculator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MigrationPlanCalculator $migrationPlanCalculator,
|
||||
private readonly MetadataStorage $metadataStorage,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getExecutedUnavailableMigrations(): ExecutedMigrationsList
|
||||
{
|
||||
$executedMigrations = $this->metadataStorage->getExecutedMigrations();
|
||||
$availableMigration = $this->migrationPlanCalculator->getMigrations();
|
||||
|
||||
return $executedMigrations->unavailableSubset($availableMigration);
|
||||
}
|
||||
|
||||
public function getNewMigrations(): AvailableMigrationsList
|
||||
{
|
||||
$executedMigrations = $this->metadataStorage->getExecutedMigrations();
|
||||
$availableMigration = $this->migrationPlanCalculator->getMigrations();
|
||||
|
||||
return $availableMigration->newSubset($executedMigrations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
use Doctrine\Migrations\EventDispatcher;
|
||||
use Doctrine\Migrations\Events;
|
||||
use Doctrine\Migrations\Exception\SkipMigration;
|
||||
use Doctrine\Migrations\Metadata\MigrationPlan;
|
||||
use Doctrine\Migrations\Metadata\Storage\MetadataStorage;
|
||||
use Doctrine\Migrations\MigratorConfiguration;
|
||||
use Doctrine\Migrations\ParameterFormatter;
|
||||
use Doctrine\Migrations\Provider\SchemaDiffProvider;
|
||||
use Doctrine\Migrations\Query\Query;
|
||||
use Doctrine\Migrations\Tools\BytesFormatter;
|
||||
use Doctrine\Migrations\Tools\TransactionHelper;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\LogLevel;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
use Throwable;
|
||||
|
||||
use function count;
|
||||
use function method_exists;
|
||||
use function ucfirst;
|
||||
|
||||
/**
|
||||
* The DbalExecutor class is responsible for executing a single migration version.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DbalExecutor implements Executor
|
||||
{
|
||||
/** @var Query[] */
|
||||
private array $sql = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly MetadataStorage $metadataStorage,
|
||||
private readonly EventDispatcher $dispatcher,
|
||||
private readonly Connection $connection,
|
||||
private readonly SchemaDiffProvider $schemaProvider,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly ParameterFormatter $parameterFormatter,
|
||||
private readonly Stopwatch $stopwatch,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return Query[] */
|
||||
public function getSql(): array
|
||||
{
|
||||
return $this->sql;
|
||||
}
|
||||
|
||||
public function addSql(Query $sqlQuery): void
|
||||
{
|
||||
$this->sql[] = $sqlQuery;
|
||||
}
|
||||
|
||||
public function execute(
|
||||
MigrationPlan $plan,
|
||||
MigratorConfiguration $configuration,
|
||||
): ExecutionResult {
|
||||
$result = new ExecutionResult($plan->getVersion(), $plan->getDirection(), new DateTimeImmutable());
|
||||
|
||||
$this->startMigration($plan, $configuration);
|
||||
|
||||
try {
|
||||
$this->executeMigration(
|
||||
$plan,
|
||||
$result,
|
||||
$configuration,
|
||||
);
|
||||
|
||||
$result->setSql($this->sql);
|
||||
} catch (SkipMigration $e) {
|
||||
$result->setSkipped(true);
|
||||
|
||||
$this->migrationEnd($e, $plan, $result, $configuration);
|
||||
} catch (Throwable $e) {
|
||||
$result->setError(true, $e);
|
||||
|
||||
$this->migrationEnd($e, $plan, $result, $configuration);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function startMigration(
|
||||
MigrationPlan $plan,
|
||||
MigratorConfiguration $configuration,
|
||||
): void {
|
||||
$this->sql = [];
|
||||
|
||||
$this->dispatcher->dispatchVersionEvent(
|
||||
Events::onMigrationsVersionExecuting,
|
||||
$plan,
|
||||
$configuration,
|
||||
);
|
||||
|
||||
if (! $plan->getMigration()->isTransactional()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only start transaction if in transactional mode
|
||||
$this->connection->beginTransaction();
|
||||
}
|
||||
|
||||
private function executeMigration(
|
||||
MigrationPlan $plan,
|
||||
ExecutionResult $result,
|
||||
MigratorConfiguration $configuration,
|
||||
): ExecutionResult {
|
||||
$stopwatchEvent = $this->stopwatch->start('execute');
|
||||
|
||||
$migration = $plan->getMigration();
|
||||
$direction = $plan->getDirection();
|
||||
|
||||
$result->setState(State::PRE);
|
||||
|
||||
$fromSchema = $this->getFromSchema($configuration);
|
||||
|
||||
$migration->{'pre' . ucfirst($direction)}($fromSchema);
|
||||
|
||||
$this->logger->info(...$this->getMigrationHeader($plan, $migration, $direction));
|
||||
|
||||
$result->setState(State::EXEC);
|
||||
|
||||
$toSchema = $this->schemaProvider->createToSchema($fromSchema);
|
||||
|
||||
$result->setToSchema($toSchema);
|
||||
|
||||
$migration->$direction($toSchema);
|
||||
|
||||
foreach ($migration->getSql() as $sqlQuery) {
|
||||
$this->addSql($sqlQuery);
|
||||
}
|
||||
|
||||
foreach ($this->schemaProvider->getSqlDiffToMigrate($fromSchema, $toSchema) as $sql) {
|
||||
$this->addSql(new Query($sql));
|
||||
}
|
||||
|
||||
$migration->freeze();
|
||||
|
||||
if (count($this->sql) !== 0) {
|
||||
if (! $configuration->isDryRun()) {
|
||||
$this->executeResult($configuration);
|
||||
} else {
|
||||
foreach ($this->sql as $query) {
|
||||
$this->outputSqlQuery($query, $configuration);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->logger->warning('Migration {version} was executed but did not result in any SQL statements.', [
|
||||
'version' => (string) $plan->getVersion(),
|
||||
]);
|
||||
}
|
||||
|
||||
$result->setState(State::POST);
|
||||
|
||||
$migration->{'post' . ucfirst($direction)}($toSchema);
|
||||
|
||||
$stopwatchEvent->stop();
|
||||
$periods = $stopwatchEvent->getPeriods();
|
||||
$lastPeriod = $periods[count($periods) - 1];
|
||||
|
||||
$result->setTime((float) $lastPeriod->getDuration() / 1000);
|
||||
$result->setMemory($lastPeriod->getMemory());
|
||||
|
||||
$params = [
|
||||
'version' => (string) $plan->getVersion(),
|
||||
'time' => $lastPeriod->getDuration(),
|
||||
'memory' => BytesFormatter::formatBytes($lastPeriod->getMemory()),
|
||||
'direction' => $direction === Direction::UP ? 'migrated' : 'reverted',
|
||||
];
|
||||
|
||||
$this->logger->info('Migration {version} {direction} (took {time}ms, used {memory} memory)', $params);
|
||||
|
||||
if (! $configuration->isDryRun()) {
|
||||
$this->metadataStorage->complete($result);
|
||||
} elseif (method_exists($this->metadataStorage, 'getSql')) {
|
||||
foreach ($this->metadataStorage->getSql($result) as $sqlQuery) {
|
||||
$this->addSql($sqlQuery);
|
||||
}
|
||||
}
|
||||
|
||||
if ($migration->isTransactional()) {
|
||||
TransactionHelper::commitIfInTransaction($this->connection);
|
||||
}
|
||||
|
||||
$plan->markAsExecuted($result);
|
||||
$result->setState(State::NONE);
|
||||
|
||||
$this->dispatcher->dispatchVersionEvent(
|
||||
Events::onMigrationsVersionExecuted,
|
||||
$plan,
|
||||
$configuration,
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @return mixed[] */
|
||||
private function getMigrationHeader(MigrationPlan $planItem, AbstractMigration $migration, string $direction): array
|
||||
{
|
||||
$versionInfo = (string) $planItem->getVersion();
|
||||
$description = $migration->getDescription();
|
||||
|
||||
if ($description !== '') {
|
||||
$versionInfo .= ' (' . $description . ')';
|
||||
}
|
||||
|
||||
$params = ['version_name' => $versionInfo];
|
||||
|
||||
if ($direction === Direction::UP) {
|
||||
return ['++ migrating {version_name}', $params];
|
||||
}
|
||||
|
||||
return ['++ reverting {version_name}', $params];
|
||||
}
|
||||
|
||||
private function migrationEnd(Throwable $e, MigrationPlan $plan, ExecutionResult $result, MigratorConfiguration $configuration): void
|
||||
{
|
||||
$migration = $plan->getMigration();
|
||||
if ($migration->isTransactional()) {
|
||||
//only rollback transaction if in transactional mode
|
||||
TransactionHelper::rollbackIfInTransaction($this->connection);
|
||||
}
|
||||
|
||||
$plan->markAsExecuted($result);
|
||||
$this->logResult($e, $result, $plan);
|
||||
|
||||
$this->dispatcher->dispatchVersionEvent(
|
||||
Events::onMigrationsVersionSkipped,
|
||||
$plan,
|
||||
$configuration,
|
||||
);
|
||||
}
|
||||
|
||||
private function logResult(Throwable $e, ExecutionResult $result, MigrationPlan $plan): void
|
||||
{
|
||||
if ($result->isSkipped()) {
|
||||
$this->logger->notice(
|
||||
'Migration {version} skipped during {state}. Reason: "{reason}"',
|
||||
[
|
||||
'version' => (string) $plan->getVersion(),
|
||||
'reason' => $e->getMessage(),
|
||||
'state' => $this->getExecutionStateAsString($result->getState()),
|
||||
],
|
||||
);
|
||||
} elseif ($result->hasError()) {
|
||||
$this->logger->error(
|
||||
'Migration {version} failed during {state}. Error: "{error}"',
|
||||
[
|
||||
'version' => (string) $plan->getVersion(),
|
||||
'error' => $e->getMessage(),
|
||||
'state' => $this->getExecutionStateAsString($result->getState()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function executeResult(MigratorConfiguration $configuration): void
|
||||
{
|
||||
foreach ($this->sql as $key => $query) {
|
||||
$this->outputSqlQuery($query, $configuration);
|
||||
|
||||
$stopwatchEvent = $this->stopwatch->start('query');
|
||||
// executeQuery() must be used here because $query might return a result set, for instance REPAIR does
|
||||
$this->connection->executeQuery($query->getStatement(), $query->getParameters(), $query->getTypes());
|
||||
$stopwatchEvent->stop();
|
||||
|
||||
if (! $configuration->getTimeAllQueries()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->logger->notice('Query took {duration}ms', [
|
||||
'duration' => $stopwatchEvent->getDuration(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function outputSqlQuery(Query $query, MigratorConfiguration $configuration): void
|
||||
{
|
||||
$params = $this->parameterFormatter->formatParameters(
|
||||
$query->getParameters(),
|
||||
$query->getTypes(),
|
||||
);
|
||||
|
||||
$this->logger->log(
|
||||
$configuration->getTimeAllQueries() ? LogLevel::NOTICE : LogLevel::DEBUG,
|
||||
'{query} {params}',
|
||||
[
|
||||
'query' => $query->getStatement(),
|
||||
'params' => $params,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
private function getFromSchema(MigratorConfiguration $configuration): Schema
|
||||
{
|
||||
// if we're in a dry run, use the from Schema instead of reading the schema from the database
|
||||
if ($configuration->isDryRun() && $configuration->getFromSchema() !== null) {
|
||||
return $configuration->getFromSchema();
|
||||
}
|
||||
|
||||
return $this->schemaProvider->createFromSchema();
|
||||
}
|
||||
|
||||
private function getExecutionStateAsString(int $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
State::PRE => 'Pre-Checks',
|
||||
State::POST => 'Post-Checks',
|
||||
State::EXEC => 'Execution',
|
||||
default => 'No State',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* The DbalMigrationFactory class is responsible for creating instances of a migration class name for a DBAL connection.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DbalMigrationFactory implements MigrationFactory
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function createVersion(string $migrationClassName): AbstractMigration
|
||||
{
|
||||
return new $migrationClassName(
|
||||
$this->connection,
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
use Doctrine\Migrations\Exception\NoMigrationsFoundWithCriteria;
|
||||
use Doctrine\Migrations\Exception\NoMigrationsToExecute;
|
||||
use Doctrine\Migrations\Exception\UnknownMigrationVersion;
|
||||
use Doctrine\Migrations\Metadata\Storage\MetadataStorage;
|
||||
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* The DefaultAliasResolver class is responsible for resolving aliases like first, current, etc. to the actual version number.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DefaultAliasResolver implements AliasResolver
|
||||
{
|
||||
private const ALIAS_FIRST = 'first';
|
||||
private const ALIAS_CURRENT = 'current';
|
||||
private const ALIAS_PREV = 'prev';
|
||||
private const ALIAS_NEXT = 'next';
|
||||
private const ALIAS_LATEST = 'latest';
|
||||
|
||||
public function __construct(
|
||||
private readonly MigrationPlanCalculator $migrationPlanCalculator,
|
||||
private readonly MetadataStorage $metadataStorage,
|
||||
private readonly MigrationStatusCalculator $migrationStatusCalculator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version number from an alias.
|
||||
*
|
||||
* Supported aliases are:
|
||||
*
|
||||
* - first: The very first version before any migrations have been run.
|
||||
* - current: The current version.
|
||||
* - prev: The version prior to the current version.
|
||||
* - next: The version following the current version.
|
||||
* - latest: The latest available version.
|
||||
*
|
||||
* If an existing version number is specified, it is returned verbatimly.
|
||||
*
|
||||
* @throws NoMigrationsToExecute
|
||||
* @throws UnknownMigrationVersion
|
||||
* @throws NoMigrationsFoundWithCriteria
|
||||
*/
|
||||
public function resolveVersionAlias(string $alias): Version
|
||||
{
|
||||
$availableMigrations = $this->migrationPlanCalculator->getMigrations();
|
||||
$executedMigrations = $this->metadataStorage->getExecutedMigrations();
|
||||
|
||||
switch ($alias) {
|
||||
case self::ALIAS_FIRST:
|
||||
case '0':
|
||||
return new Version('0');
|
||||
|
||||
case self::ALIAS_CURRENT:
|
||||
try {
|
||||
return $executedMigrations->getLast()->getVersion();
|
||||
} catch (NoMigrationsFoundWithCriteria) {
|
||||
return new Version('0');
|
||||
}
|
||||
|
||||
// no break because of return
|
||||
case self::ALIAS_PREV:
|
||||
try {
|
||||
return $executedMigrations->getLast(-1)->getVersion();
|
||||
} catch (NoMigrationsFoundWithCriteria) {
|
||||
return new Version('0');
|
||||
}
|
||||
|
||||
// no break because of return
|
||||
case self::ALIAS_NEXT:
|
||||
$newMigrations = $this->migrationStatusCalculator->getNewMigrations();
|
||||
|
||||
try {
|
||||
return $newMigrations->getFirst()->getVersion();
|
||||
} catch (NoMigrationsFoundWithCriteria $e) {
|
||||
throw NoMigrationsToExecute::new($e);
|
||||
}
|
||||
|
||||
// no break because of return
|
||||
case self::ALIAS_LATEST:
|
||||
try {
|
||||
return $availableMigrations->getLast()->getVersion();
|
||||
} catch (NoMigrationsFoundWithCriteria) {
|
||||
return $this->resolveVersionAlias(self::ALIAS_CURRENT);
|
||||
}
|
||||
|
||||
// no break because of return
|
||||
default:
|
||||
if ($availableMigrations->hasMigration(new Version($alias))) {
|
||||
return $availableMigrations->getMigration(new Version($alias))->getVersion();
|
||||
}
|
||||
|
||||
if (substr($alias, 0, 7) === self::ALIAS_CURRENT) {
|
||||
$val = (int) substr($alias, 7);
|
||||
$targetMigration = null;
|
||||
if ($val > 0) {
|
||||
$newMigrations = $this->migrationStatusCalculator->getNewMigrations();
|
||||
|
||||
return $newMigrations->getFirst($val - 1)->getVersion();
|
||||
}
|
||||
|
||||
return $executedMigrations->getLast($val)->getVersion();
|
||||
}
|
||||
}
|
||||
|
||||
throw UnknownMigrationVersion::new($alias);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
/**
|
||||
* The Direction class contains constants for the directions a migration can be executed.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Direction
|
||||
{
|
||||
public const UP = 'up';
|
||||
public const DOWN = 'down';
|
||||
|
||||
/**
|
||||
* This class cannot be instantiated.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\Query\Query;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* The ExecutionResult class is responsible for storing the result of a migration version after it executes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ExecutionResult
|
||||
{
|
||||
/** @var Query[] */
|
||||
private array $sql = [];
|
||||
|
||||
/**
|
||||
* Seconds
|
||||
*/
|
||||
private float|null $time = null;
|
||||
|
||||
private float|null $memory = null;
|
||||
|
||||
private bool $skipped = false;
|
||||
|
||||
private bool $error = false;
|
||||
|
||||
private Throwable|null $exception = null;
|
||||
|
||||
private int $state;
|
||||
|
||||
private Schema|null $toSchema = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly Version $version,
|
||||
private readonly string $direction = Direction::UP,
|
||||
private DateTimeImmutable|null $executedAt = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getDirection(): string
|
||||
{
|
||||
return $this->direction;
|
||||
}
|
||||
|
||||
public function getExecutedAt(): DateTimeImmutable|null
|
||||
{
|
||||
return $this->executedAt;
|
||||
}
|
||||
|
||||
public function setExecutedAt(DateTimeImmutable $executedAt): void
|
||||
{
|
||||
$this->executedAt = $executedAt;
|
||||
}
|
||||
|
||||
public function getVersion(): Version
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
public function hasSql(): bool
|
||||
{
|
||||
return count($this->sql) !== 0;
|
||||
}
|
||||
|
||||
/** @return Query[] */
|
||||
public function getSql(): array
|
||||
{
|
||||
return $this->sql;
|
||||
}
|
||||
|
||||
/** @param Query[] $sql */
|
||||
public function setSql(array $sql): void
|
||||
{
|
||||
$this->sql = $sql;
|
||||
}
|
||||
|
||||
public function getTime(): float|null
|
||||
{
|
||||
return $this->time;
|
||||
}
|
||||
|
||||
public function setTime(float $time): void
|
||||
{
|
||||
$this->time = $time;
|
||||
}
|
||||
|
||||
public function getMemory(): float|null
|
||||
{
|
||||
return $this->memory;
|
||||
}
|
||||
|
||||
public function setMemory(float $memory): void
|
||||
{
|
||||
$this->memory = $memory;
|
||||
}
|
||||
|
||||
public function setSkipped(bool $skipped): void
|
||||
{
|
||||
$this->skipped = $skipped;
|
||||
}
|
||||
|
||||
public function isSkipped(): bool
|
||||
{
|
||||
return $this->skipped;
|
||||
}
|
||||
|
||||
public function setError(bool $error, Throwable|null $exception = null): void
|
||||
{
|
||||
$this->error = $error;
|
||||
$this->exception = $exception;
|
||||
}
|
||||
|
||||
public function hasError(): bool
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function getException(): Throwable|null
|
||||
{
|
||||
return $this->exception;
|
||||
}
|
||||
|
||||
public function setToSchema(Schema $toSchema): void
|
||||
{
|
||||
$this->toSchema = $toSchema;
|
||||
}
|
||||
|
||||
public function getToSchema(): Schema
|
||||
{
|
||||
if ($this->toSchema === null) {
|
||||
throw new RuntimeException('Cannot call getToSchema() when toSchema is null.');
|
||||
}
|
||||
|
||||
return $this->toSchema;
|
||||
}
|
||||
|
||||
public function getState(): int
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
public function setState(int $state): void
|
||||
{
|
||||
$this->state = $state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
use Doctrine\Migrations\Metadata\MigrationPlan;
|
||||
use Doctrine\Migrations\MigratorConfiguration;
|
||||
use Doctrine\Migrations\Query\Query;
|
||||
|
||||
/**
|
||||
* The Executor defines the interface used for adding sql for a migration and executing that sql.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface Executor
|
||||
{
|
||||
public function addSql(Query $sqlQuery): void;
|
||||
|
||||
public function execute(MigrationPlan $plan, MigratorConfiguration $migratorConfiguration): ExecutionResult;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* The MigrationFactory is responsible for creating instances of the migration class name.
|
||||
*/
|
||||
interface MigrationFactory
|
||||
{
|
||||
public function createVersion(string $migrationClassName): AbstractMigration;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
use Doctrine\Migrations\Metadata\AvailableMigrationsList;
|
||||
use Doctrine\Migrations\Metadata\MigrationPlanList;
|
||||
|
||||
/**
|
||||
* The MigrationPlanCalculator is responsible for calculating the plan for migrating from the current
|
||||
* version to another version.
|
||||
*/
|
||||
interface MigrationPlanCalculator
|
||||
{
|
||||
/** @param Version[] $versions */
|
||||
public function getPlanForVersions(array $versions, string $direction): MigrationPlanList;
|
||||
|
||||
public function getPlanUntilVersion(Version $to): MigrationPlanList;
|
||||
|
||||
/**
|
||||
* Returns a sorted list of migrations.
|
||||
*/
|
||||
public function getMigrations(): AvailableMigrationsList;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
use Doctrine\Migrations\Metadata\AvailableMigrationsList;
|
||||
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
|
||||
|
||||
/**
|
||||
* The MigrationStatusCalculator is responsible for calculating the current status of
|
||||
* migrated and not available versions.
|
||||
*/
|
||||
interface MigrationStatusCalculator
|
||||
{
|
||||
public function getExecutedUnavailableMigrations(): ExecutedMigrationsList;
|
||||
|
||||
public function getNewMigrations(): AvailableMigrationsList;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
use Doctrine\Migrations\Exception\MigrationClassNotFound;
|
||||
use Doctrine\Migrations\Metadata;
|
||||
use Doctrine\Migrations\Metadata\AvailableMigration;
|
||||
use Doctrine\Migrations\Metadata\AvailableMigrationsList;
|
||||
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
|
||||
use Doctrine\Migrations\Metadata\MigrationPlan;
|
||||
use Doctrine\Migrations\Metadata\MigrationPlanList;
|
||||
use Doctrine\Migrations\Metadata\Storage\MetadataStorage;
|
||||
use Doctrine\Migrations\MigrationsRepository;
|
||||
|
||||
use function array_diff;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function array_reverse;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function reset;
|
||||
use function uasort;
|
||||
|
||||
/**
|
||||
* The MigrationPlanCalculator is responsible for calculating the plan for migrating from the current
|
||||
* version to another version.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class SortedMigrationPlanCalculator implements MigrationPlanCalculator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MigrationsRepository $migrationRepository,
|
||||
private readonly MetadataStorage $metadataStorage,
|
||||
private readonly Comparator $sorter,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @param Version[] $versions */
|
||||
public function getPlanForVersions(array $versions, string $direction): MigrationPlanList
|
||||
{
|
||||
$migrationsToCheck = $this->arrangeMigrationsForDirection($direction, $this->getMigrations());
|
||||
$availableMigrations = array_filter(
|
||||
$migrationsToCheck,
|
||||
// in_array third parameter is intentionally false to force object to string casting
|
||||
static fn (AvailableMigration $availableMigration): bool => in_array($availableMigration->getVersion(), $versions, false),
|
||||
);
|
||||
|
||||
$planItems = array_map(static fn (AvailableMigration $availableMigration): MigrationPlan => new MigrationPlan($availableMigration->getVersion(), $availableMigration->getMigration(), $direction), $availableMigrations);
|
||||
|
||||
if (count($planItems) !== count($versions)) {
|
||||
$plannedVersions = array_map(static fn (MigrationPlan $migrationPlan): Version => $migrationPlan->getVersion(), $planItems);
|
||||
$diff = array_diff($versions, $plannedVersions);
|
||||
|
||||
throw MigrationClassNotFound::new((string) reset($diff));
|
||||
}
|
||||
|
||||
return new MigrationPlanList($planItems, $direction);
|
||||
}
|
||||
|
||||
public function getPlanUntilVersion(Version $to): MigrationPlanList
|
||||
{
|
||||
if ((string) $to !== '0' && ! $this->migrationRepository->hasMigration((string) $to)) {
|
||||
throw MigrationClassNotFound::new((string) $to);
|
||||
}
|
||||
|
||||
$availableMigrations = $this->getMigrations(); // migrations are sorted at this point
|
||||
$executedMigrations = $this->metadataStorage->getExecutedMigrations();
|
||||
|
||||
$direction = $this->findDirection($to, $executedMigrations, $availableMigrations);
|
||||
|
||||
$migrationsToCheck = $this->arrangeMigrationsForDirection($direction, $availableMigrations);
|
||||
|
||||
$toExecute = $this->findMigrationsToExecute($to, $migrationsToCheck, $direction, $executedMigrations);
|
||||
|
||||
return new MigrationPlanList(array_map(static fn (AvailableMigration $migration): MigrationPlan => new MigrationPlan($migration->getVersion(), $migration->getMigration(), $direction), $toExecute), $direction);
|
||||
}
|
||||
|
||||
public function getMigrations(): AvailableMigrationsList
|
||||
{
|
||||
$availableMigrations = $this->migrationRepository->getMigrations()->getItems();
|
||||
uasort($availableMigrations, fn (AvailableMigration $a, AvailableMigration $b): int => $this->sorter->compare($a->getVersion(), $b->getVersion()));
|
||||
|
||||
return new AvailableMigrationsList($availableMigrations);
|
||||
}
|
||||
|
||||
private function findDirection(Version $to, ExecutedMigrationsList $executedMigrations, AvailableMigrationsList $availableMigrations): string
|
||||
{
|
||||
if ((string) $to === '0') {
|
||||
return Direction::DOWN;
|
||||
}
|
||||
|
||||
foreach ($availableMigrations->getItems() as $availableMigration) {
|
||||
if ($availableMigration->getVersion()->equals($to)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (! $executedMigrations->hasMigration($availableMigration->getVersion())) {
|
||||
return Direction::UP;
|
||||
}
|
||||
}
|
||||
|
||||
if ($executedMigrations->hasMigration($to) && ! $executedMigrations->getLast()->getVersion()->equals($to)) {
|
||||
return Direction::DOWN;
|
||||
}
|
||||
|
||||
return Direction::UP;
|
||||
}
|
||||
|
||||
/** @return AvailableMigration[] */
|
||||
private function arrangeMigrationsForDirection(string $direction, Metadata\AvailableMigrationsList $availableMigrations): array
|
||||
{
|
||||
return $direction === Direction::UP ? $availableMigrations->getItems() : array_reverse($availableMigrations->getItems());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AvailableMigration[] $migrationsToCheck
|
||||
*
|
||||
* @return AvailableMigration[]
|
||||
*/
|
||||
private function findMigrationsToExecute(Version $to, array $migrationsToCheck, string $direction, ExecutedMigrationsList $executedMigrations): array
|
||||
{
|
||||
$toExecute = [];
|
||||
foreach ($migrationsToCheck as $availableMigration) {
|
||||
if ($direction === Direction::DOWN && $availableMigration->getVersion()->equals($to)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($direction === Direction::UP && ! $executedMigrations->hasMigration($availableMigration->getVersion())) {
|
||||
$toExecute[] = $availableMigration;
|
||||
} elseif ($direction === Direction::DOWN && $executedMigrations->hasMigration($availableMigration->getVersion())) {
|
||||
$toExecute[] = $availableMigration;
|
||||
}
|
||||
|
||||
if ($direction === Direction::UP && $availableMigration->getVersion()->equals($to)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $toExecute;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
/**
|
||||
* The State class contains constants for the different states a migration can be in during execution.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class State
|
||||
{
|
||||
public const NONE = 0;
|
||||
|
||||
public const PRE = 1;
|
||||
|
||||
public const EXEC = 2;
|
||||
|
||||
public const POST = 3;
|
||||
|
||||
public const STATES = [
|
||||
self::NONE,
|
||||
self::PRE,
|
||||
self::EXEC,
|
||||
self::POST,
|
||||
];
|
||||
|
||||
/**
|
||||
* This class cannot be instantiated.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Migrations\Version;
|
||||
|
||||
use Stringable;
|
||||
|
||||
final class Version implements Stringable
|
||||
{
|
||||
public function __construct(private readonly string $version)
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
public function equals(mixed $object): bool
|
||||
{
|
||||
return $object instanceof self && $object->version === $this->version;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user