RebelCode/sql-migrations

View on GitHub
src/AbstractMigrator.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

namespace RebelCode\Storage\Migration\Sql;

use Exception;
use RuntimeException;
use Traversable;

/**
 * Common functionality for migrator implementations.
 *
 * @since [*next-version*]
 */
abstract class AbstractMigrator implements MigratorInterface
{
    /**
     * {@inheritdoc}
     *
     * @since [*next-version*]
     */
    public function migrate($targetVer)
    {
        $versions = $this->getMigrationInfo($targetVer, $direction);

        foreach ($versions as $version) {
            $uMigrations = $this->getMigrations($version, $direction);
            $sMigrations = $this->sortMigrations($uMigrations, $direction);

            try {
                $this->runMigrations($version, $direction, $sMigrations);
            } catch (RuntimeException $exception) {
                $this->onMigrationsError($version, $direction, $sMigrations, $exception);
            }
        }
    }

    /**
     * Retrieves the versions that need to be processed to migrate to a particular target version.
     *
     * @since [*next-version*]
     *
     * @param int      $targetVer The target version.
     * @param int|null $direction This will be set to a positive integer for "up" migrations, a negative integer for
     *                            "down" migrations or zero if no migration is required.
     *
     * @return int[]|string[] An array of versions that need to be processed.
     */
    protected function getMigrationInfo($targetVer, &$direction = null)
    {
        $targetVer = max(0, (int) $targetVer);
        $currVer   = max(0, (int) $this->getCurrentVersion());
        $direction = $targetVer - $currVer;

        if ($direction === 0) {
            return [];
        }

        return ($direction > 0)
            ? range($currVer + 1, $targetVer)  // up
            : range($currVer, $targetVer + 1); // down
    }

    /**
     * Runs a list of migrations, rolling back any run migrations if at least one fails.
     *
     * @since [*next-version*]
     *
     * @param int                              $version    An integer version number to migration to.
     * @param int                              $direction  A positive integer for "up" migrations or a negative integer
     *                                                     for "down" migrations.
     * @param MigrationInterface[]|Traversable $migrations The list of migration instances to run.
     *
     * @throws RuntimeException If an error occurred while migrating the database.
     */
    protected function runMigrations($version, $direction, $migrations)
    {
        $runMigrations = [];

        // Run the migrations and record each successful migration in case of a failure and a required rollback
        try {
            foreach ($migrations as $migration) {
                $this->doMigration($migration, $version, $direction);
                $runMigrations[] = $migration;
            }
        } catch (Exception $exception) {
            $details = sprintf('"%1$s" %2$s', $migration->getKey(), ($direction > 0) ? 'up' : 'down');

            try {
                $this->rollbackMigrations($version, $direction, $runMigrations);
            } catch (RuntimeException $rollbackException) {
                throw new RuntimeException(
                    sprintf('The %s migration and subsequent rollback were unsuccessful', $details),
                    0,
                    $rollbackException
                );
            }

            throw new RuntimeException(
                sprintf('The %s migration failed and rollback was successful', $details),
                0,
                $exception
            );
        }
    }

    /**
     * Rolls back any changes made by a list of run migrations,.
     *
     * @since [*next-version*]
     *
     * @param int                              $version    An integer version number to migration to.
     * @param int                              $direction  A positive integer for "up" migrations or a negative integer
     *                                                     for "down" migrations.
     * @param MigrationInterface[]|Traversable $migrations The list of migration instances that were successfully run
     *                                                     prior to the failure.
     */
    protected function rollbackMigrations($version, $direction, $migrations)
    {
        // Run the migrations in the reverse direction
        foreach ($migrations as $migration) {
            try {
                $this->doMigration($migration, $version, $direction * -1);
            } catch (Exception $rException) {
                $dirStr = ($direction > 0) ? 'up' : 'down';
                $migKey = $migration->getKey();
                throw new RuntimeException(
                    sprintf('Rollback failed for the "%1$s" %2$s migration', $migKey, $dirStr),
                    null,
                    $rException
                );
            }
        }
    }

    /**
     * Sorts the migrations by their priority.
     *
     * @since [*next-version*]
     *
     * @param MigrationInterface[]|Traversable $migrations The list of migrations to sort.
     * @param int                              $direction  A positive integer for "up" migrations or a negative integer
     *                                                     for "down" migrations.
     *
     * @return MigrationInterface[] The sorted migration instance.
     */
    protected function sortMigrations(
        $migrations,
        $direction = 1
    ) {
        // Reduce direction to 1 (up) or -1 (down)
        $multiplier = (int) ($direction / abs($direction));
        // If an iterator, change to array to be able to use usort()
        $array = is_array($migrations) ? $migrations : iterator_to_array($migrations);

        usort($array, function (MigrationInterface $migrationA, MigrationInterface $migrationB) use ($multiplier) {
            // The multiplier is used to reverse the sorting if the direction is down
            return ($migrationA->getPriority() - $migrationB->getPriority()) * $multiplier;
        });

        return $array;
    }

    /**
     * Runs a migration in a particular direction.
     *
     * @since [*next-version*]
     *
     * @param MigrationInterface $migration The migration instance.
     * @param int                $version   An integer version number to migration to.
     * @param int                $direction A positive integer for "up" migrations or a negative integer for "down"
     *                                      migrations.
     *
     * @throws RuntimeException If the query failed.
     */
    protected function doMigration(
        MigrationInterface $migration,
        $version,
        $direction
    ) {
        $query = ($direction > 0) ? $migration->getUpQuery() : $migration->getDownQuery();

        $this->runQuery($query);
    }

    /**
     * Invoked when migration fails due to some error.
     *
     * @since [*next-version*]
     *
     * @param int                              $version    An integer version number.
     * @param int                              $direction  A positive integer for "up" migrations or a negative integer
     *                                                     for "down" migrations.
     * @param MigrationInterface[]|Traversable $migrations The list of migration instances that were run.
     * @param RuntimeException                 $exception  The exception that was thrown.
     */
    protected function onMigrationsError(
        $version,
        $direction,
        $migrations,
        RuntimeException $exception
    ) {
        throw $exception;
    }

    /**
     * Retrieves the database's current version.
     *
     * @since [*next-version*]
     *
     * @return int An integer version number to migration to.
     */
    abstract protected function getCurrentVersion();

    /**
     * Retrieves the migrations for a particular version in a particular direction.
     *
     * @since [*next-version*]
     *
     * @param int $version   An integer version number.
     * @param int $direction A positive integer for "up" migrations or a negative integer for "down" migrations.
     *
     * @return MigrationInterface[]|Traversable The migration instances.
     */
    abstract protected function getMigrations($version, $direction);

    /**
     * Runs an SQL query.
     *
     * @since [*next-version*]
     *
     * @param string $query The query.
     *
     * @throws RuntimeException If the query failed.
     */
    abstract protected function runQuery($query);
}