1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 
<?php
namespace Omeka\Db\Migration;

use Doctrine\DBAL\Connection;
use GlobIterator;
use PDO;
use Zend\I18n\Translator\TranslatorInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

/**
 * Migration manager.
 */
class Manager
{
    /**
     * @var string
     */
    protected $path;

    /**
     * @var string
     */
    protected $namespace;

    /**
     * @var TranslatorInterface
     */
    protected $translator;

    /**
     * @var ServiceLocatorInterface
     */
    protected $serviceLocator;

    /**
     * @var Connection
     */
    protected $conn;

    /**
     * Create the migration manager, passing configuration.
     *
     * @param array $config
     * @param Connection $conn;
     * @param ServiceLocatorInterface $serviceLocator
     */
    public function __construct($config, Connection $conn, ServiceLocatorInterface $serviceLocator)
    {
        $this->setConfig($config);
        $this->conn = $conn;
        $this->serviceLocator = $serviceLocator;
    }

    /**
     * Set config options from array.
     *
     * @param array $config
     */
    public function setConfig($config)
    {
        if (array_key_exists('path', $config)) {
            $this->path = $config['path'];
        }

        if (array_key_exists('namespace', $config)) {
            $this->namespace = $config['namespace'];
        }
    }

    /**
     * Perform the upgrade operation on all pending migrations.
     */
    public function upgrade()
    {
        $toPerform = $this->getMigrationsToPerform();

        foreach ($toPerform as $version => $migrationInfo) {
            $migration = $this->loadMigration(
                $migrationInfo['path'], $migrationInfo['class']);
            $migration->up($this->conn);
            $this->recordMigration($version);
        }

        $this->clearDoctrineCache();
    }

    /**
     * Record a migration as complete in the database.
     *
     * @param string $version Version to record
     */
    public function recordMigration($version)
    {
        $this->conn->insert('migration', ['version' => $version]);
    }

    /**
     * Load a migration file and instantiate the class.
     *
     * @param string $path Path to the migration
     * @param string $class Fully-qualified name of the migration class
     
     */
    public function loadMigration($path, $class)
    {
        require_once $path;

        if (!class_exists($class, false)
            || !is_subclass_of($class, 'Omeka\Db\Migration\MigrationInterface')
        ) {
            throw new Exception\ClassNotFoundException(
                $this->getTranslator()->translate('Migration file did not contain the expected class')
            );
        }

        if (is_subclass_of($class, 'Omeka\Db\Migration\ConstructedMigrationInterface')) {
            $migration = $class::create($this->serviceLocator);
        } else {
            $migration = new $class;
        }

        return $migration;
    }

    /**
     * Get pending migrations.
     *
     * @return array
     */
    public function getMigrationsToPerform()
    {
        $available = $this->getAvailableMigrations();
        $completed = $this->getCompletedMigrations();
        $diff = array_diff_key($available, array_flip($completed));
        ksort($diff);
        return $diff;
    }

    /**
     * Get already-performed migrations.
     *
     * @return array
     */
    public function getCompletedMigrations()
    {
        $completed = $this->conn
            ->executeQuery("SELECT version FROM migration")
            ->fetchAll(PDO::FETCH_COLUMN);
        if (!$completed) {
            $completed = [];
        }
        return $completed;
    }

    /**
     * Get available migrations.
     *
     * @return array
     */
    public function getAvailableMigrations()
    {
        $migrations = [];
        $globPattern = $this->path . DIRECTORY_SEPARATOR . '*.php';
        $regexPattern = '/^(\d+)_(\w+)\.php$/';
        $iterator = new GlobIterator($globPattern);
        foreach ($iterator as $fileInfo) {
            $filename = $fileInfo->getFilename();
            if (preg_match($regexPattern, $fileInfo->getFilename(), $matches)) {
                $version = $matches[1];
                $class = $this->namespace . '\\' . $matches[2];
                $migrations[$version] = [
                    'path' => $fileInfo->getPathname(),
                    'class' => $class,
                ];
            }
        }

        return $migrations;
    }

    /**
     * Get the translator service
     *
     * return TranslatorInterface
     */
    public function getTranslator()
    {
        if (!$this->translator instanceof TranslatorInterface) {
            $this->translator = $this->serviceLocator->get('MvcTranslator');
        }
        return $this->translator;
    }

    /**
     * Clear Doctrine's cache to prevent errors after upgrade.
     */
    protected function clearDoctrineCache()
    {
        $em = $this->serviceLocator->get('Omeka\EntityManager');
        $cache = $em->getConfiguration()->getMetadataCacheImpl();

        if (!$cache) {
            return;
        }

        $cache->deleteAll();
    }
}