RebelCode/rcmod-wp-bookings-cqrs

View on GitHub
src/Module/Migrator.php

Summary

Maintainability
A
35 mins
Test Coverage
<?php

namespace RebelCode\Storage\Resource\WordPress\Module;

use ArrayAccess;
use Dhii\Data\Container\NormalizeContainerCapableTrait;
use Dhii\Data\Object\DataStoreAwareContainerTrait;
use Dhii\Exception\CreateInvalidArgumentExceptionCapableTrait;
use Dhii\Exception\CreateOutOfRangeExceptionCapableTrait;
use Dhii\Exception\CreateRuntimeExceptionCapableTrait;
use Dhii\I18n\StringTranslatingTrait;
use Dhii\Output\TemplateFactoryInterface;
use Dhii\Util\Normalization\NormalizeIntCapableTrait;
use Dhii\Util\Normalization\NormalizeStringCapableTrait;
use Dhii\Util\String\StringableInterface as Stringable;
use InvalidArgumentException;
use mysqli;
use Psr\Container\ContainerInterface;
use RuntimeException;
use stdClass;

/**
 * Performs database migrations through WPDB.
 *
 * @since [*next-version*]
 */
class Migrator
{
    /* @since [*next-version*] */
    use DataStoreAwareContainerTrait {
        _getDataStore as _getPlaceholderValues;
        _setDataStore as _setPlaceholderValues;
    }

    /* @since [*next-version*] */
    use NormalizeIntCapableTrait;

    /* @since [*next-version*] */
    use NormalizeStringCapableTrait;

    /* @since [*next-version*] */
    use NormalizeContainerCapableTrait;

    /* @since [*next-version*] */
    use CreateInvalidArgumentExceptionCapableTrait;

    /* @since [*next-version*] */
    use CreateOutOfRangeExceptionCapableTrait;

    /* @since [*next-version*] */
    use CreateRuntimeExceptionCapableTrait;

    /* @since [*next-version*] */
    use StringTranslatingTrait;

    /**
     * The filename for "up" migrations.
     *
     * @since [*next-version*]
     */
    const UP_MIGRATION_FILENAME = 'up.sql';

    /**
     * The filename for "down" migrations.
     *
     * @since [*next-version*]
     */
    const DOWN_MIGRATION_FILENAME = 'down.sql';

    /**
     * The direction value for migrating upwards (upgrading).
     *
     * @since [*next-version*]
     */
    const DIRECTION_UP = 1;

    /**
     * The direction value for migrating downwards (downgrading).
     *
     * @since [*next-version*]
     */
    const DIRECTION_DOWN = -1;

    /**
     * The migrations directory path.
     *
     * @since [*next-version*]
     *
     * @var string|Stringable
     */
    protected $migrationsDir;

    /**
     * The current database version.
     *
     * @since [*next-version*]
     *
     * @var int|string|Stringable
     */
    protected $dbVersion;

    /**
     * The mysqli handle.
     *
     * @since [*next-version*]
     *
     * @var mysqli
     */
    protected $mysqli;

    /**
     * The template factory.
     *
     * @since [*next-version*]
     *
     * @var TemplateFactoryInterface|null
     */
    protected $templateFactory;

    /**
     * Constructor.
     *
     * @since [*next-version*]
     *
     * @param mysqli                                        $mysqli            The mysqli handle.
     * @param string|Stringable                             $migrationsDir     The migrations directory path.
     * @param int|string|Stringable                         $dbVersion         The current database version.
     * @param TemplateFactoryInterface                      $templateFactory   The factory for creating SQL templates.
     * @param array|stdClass|ArrayAccess|ContainerInterface $placeholderValues The replacement values for placeholders
     *                                                                         in SQL templates.
     */
    public function __construct(
        $mysqli,
        $migrationsDir,
        $dbVersion,
        TemplateFactoryInterface $templateFactory,
        $placeholderValues
    ) {
        $this->mysqli = $mysqli;
        $this->_setMigrationsDir($migrationsDir);
        $this->_setDbVersion($dbVersion);
        $this->_setTemplateFactory($templateFactory);
        $this->_setPlaceholderValues($placeholderValues);
    }

    /**
     * Retrieves the mysqli handle.
     *
     * @since [*next-version*]
     *
     * @return mysqli The mysqli handle.
     */
    protected function _getMysqli()
    {
        return $this->mysqli;
    }

    /**
     * Sets the mysqli handle.
     *
     * @since [*next-version*]
     *
     * @param mysqli $mysqli The mysqli handle.
     */
    protected function _setMysqli($mysqli)
    {
        if (!($mysqli instanceof mysqli)) {
            throw $this->_createInvalidArgumentException(
                $this->__('Argument is not a mysqli handle'), null, null, $mysqli
            );
        }

        $this->mysqli = $mysqli;
    }

    /**
     * Retrieves the migrations directory path.
     *
     * @since [*next-version*]
     *
     * @return string|Stringable The migrations directory path.
     */
    protected function _getMigrationsDir()
    {
        return $this->migrationsDir;
    }

    /**
     * Sets the migrations directory path.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable $migrationsDir The migrations directory path.
     */
    protected function _setMigrationsDir($migrationsDir)
    {
        $this->migrationsDir = $this->_normalizeString($migrationsDir);
    }

    /**
     * Retrieves the current database version.
     *
     * @since [*next-version*]
     *
     * @return int|string|Stringable The current database version.
     */
    protected function _getDbVersion()
    {
        return $this->dbVersion;
    }

    /**
     * Sets the current database version.
     *
     * @since [*next-version*]
     *
     * @param int|string |Stringable $dbVersion The current database version.
     */
    protected function _setDbVersion($dbVersion)
    {
        $this->dbVersion = $this->_normalizeInt($dbVersion);
    }

    /**
     * Retrieves the template factory.
     *
     * @since [*next-version*]
     *
     * @return TemplateFactoryInterface|null The template factory instance.
     */
    protected function _getTemplateFactory()
    {
        return $this->templateFactory;
    }

    /**
     * Retrieves the template factory.
     *
     * @since [*next-version*]
     *
     * @param TemplateFactoryInterface|null $templateFactory The template factory instance.
     */
    protected function _setTemplateFactory($templateFactory)
    {
        if ($templateFactory !== null && !($templateFactory instanceof TemplateFactoryInterface)) {
            throw $this->_createInvalidArgumentException(
                $this->__('Argument is not a template factory'), null, null, $templateFactory
            );
        }

        $this->templateFactory = $templateFactory;
    }

    /**
     * Performs database migration.
     *
     * @since [*next-version*]
     *
     * @param int|string|Stringable $target The migration target - can be a version, state, preset, etc.
     *
     * @throws InvalidArgumentException If the given target is invalid.
     * @throws RuntimeException If failed to migrate to the given target.
     */
    public function migrate($target)
    {
        $target     = $this->_normalizeInt($target);
        $current    = $this->_getDbVersion();
        $difference = $target - $current;

        // No migration needed
        if ($difference === 0) {
            return;
        }

        // Maximise the values to 0, since negative DB versions are not allowed
        $current = max(0, $current);
        $target  = max(0, $target);

        // Get direction, 1 for up and -1 for down
        $direction = (int) (absint($difference) / $difference);
        // Get the list of migration versions to run
        $migrations = ($direction === static::DIRECTION_UP)
            ? range($current + 1, $target)
            : range($current, $target + 1);
        // Determine the file names to look for, depending on migration direction
        $filename = ($direction === static::DIRECTION_UP)
            ? static::UP_MIGRATION_FILENAME
            : static::DOWN_MIGRATION_FILENAME;
        // The root migrations directory
        $directory = $this->_getMigrationsDir();

        foreach ($migrations as $_version) {
            $_path = implode(DIRECTORY_SEPARATOR, [$directory, sprintf('%1$s-%2$s', $_version, $filename)]);

            $this->_runMigrationFile($_path);
        }
    }

    /**
     * Runs the migration at a given file path.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable $filePath The path to the migration file.
     *
     * @throws RuntimeException If failed to read the migration file or if the migration failed to execute.
     */
    protected function _runMigrationFile($filePath)
    {
        $mysqli = $this->_getMysqli();
        $mysqli->autocommit(false);

        $migrationSql = $this->_readSqlMigrationFile($filePath);
        $migrationSql = $this->_replaceSqlTokens($migrationSql, $this->_getPlaceholderValues());
        $sqlQueries   = explode(';', $migrationSql);
        $sqlQueries   = array_filter(array_map('trim', $sqlQueries));
        $errors       = [];

        $mysqli->begin_transaction();

        foreach ($sqlQueries as $query) {
            $result = $mysqli->query($query . ';');

            if (!(bool) $result) {
                $errors[] = $mysqli->error;
            }
        }

        // If errors occurred, roll back the database
        if (!empty($errors)) {
            $mysqli->rollback();

            throw $this->_createRuntimeException(implode("\n", $errors));
        }

        // If there were no errors, commit the transaction
        $mysqli->commit();
    }

    /**
     * Replaces placeholder tokens in the SQL.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable                             $sql    The SQL.
     * @param array|stdClass|ArrayAccess|ContainerInterface $values The placeholder values.
     *
     * @return string|Stringable The SQL with the replaced placeholder tokens.
     */
    protected function _replaceSqlTokens($sql, $values)
    {
        $factory = $this->_getTemplateFactory();

        if (!($factory instanceof TemplateFactoryInterface)) {
            throw $this->_createRuntimeException($this->__('Template factory is null'));
        }

        $template = $factory->make([
            TemplateFactoryInterface::K_TEMPLATE => $sql
        ]);

        return $template->render($values);
    }

    /**
     * Reads the SQL from a migration file.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable $filePath The path to the migration file.
     *
     * @return string|Stringable The read SQL.
     *
     * @throws RuntimeException If failed to read the migration file.
     */
    protected function _readSqlMigrationFile($filePath)
    {
        $filePath = $this->_normalizeString($filePath);

        if (is_file($filePath) && is_readable($filePath)) {
            return file_get_contents($filePath);
        }

        throw $this->_createRuntimeException($this->__('Cannot read migration file "%s"', [$filePath]), null, null);
    }
}