2018-05-16 20:18:30 +02:00
|
|
|
<?php
|
|
|
|
namespace Misuzu;
|
|
|
|
|
|
|
|
use Exception;
|
|
|
|
use PDO;
|
|
|
|
use PDOException;
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
final class DatabaseMigrationManager {
|
2018-05-16 20:18:30 +02:00
|
|
|
private $targetConnection;
|
|
|
|
private $migrationStorage;
|
|
|
|
|
|
|
|
private const MIGRATION_NAMESPACE = '\\Misuzu\\DatabaseMigrations\\%s\\%s';
|
|
|
|
|
|
|
|
private $errors = [];
|
|
|
|
|
|
|
|
private $logFunction;
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
public function __construct(PDO $conn, string $path) {
|
2018-05-16 20:18:30 +02:00
|
|
|
$this->targetConnection = $conn;
|
|
|
|
$this->migrationStorage = realpath($path);
|
|
|
|
}
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
private function addError(Exception $exception): void {
|
2018-05-16 20:18:30 +02:00
|
|
|
$this->errors[] = $exception;
|
|
|
|
$this->writeLog($exception->getMessage());
|
|
|
|
}
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
public function setLogger(callable $logger): void {
|
2018-05-16 20:18:30 +02:00
|
|
|
$this->logFunction = $logger;
|
|
|
|
}
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
private function writeLog(string $log): void {
|
|
|
|
if(!is_callable($this->logFunction)) {
|
2018-05-16 20:18:30 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
call_user_func($this->logFunction, $log);
|
|
|
|
}
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
public function getErrors(): array {
|
2018-05-16 20:18:30 +02:00
|
|
|
return $this->errors;
|
|
|
|
}
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
private function getMigrationScripts(): array {
|
|
|
|
if(!file_exists($this->migrationStorage) || !is_dir($this->migrationStorage)) {
|
2018-05-16 20:18:30 +02:00
|
|
|
$this->addError(new Exception('Migrations script directory does not exist.'));
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
$files = glob(rtrim($this->migrationStorage, '/\\') . '/*.php');
|
|
|
|
return $files;
|
|
|
|
}
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
private function createMigrationRepository(): bool {
|
2018-05-16 20:18:30 +02:00
|
|
|
try {
|
|
|
|
$this->targetConnection->exec('
|
|
|
|
CREATE TABLE IF NOT EXISTS `msz_migrations` (
|
|
|
|
`migration_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
|
|
`migration_name` VARCHAR(255) NOT NULL,
|
|
|
|
`migration_batch` INT(11) UNSIGNED NOT NULL,
|
|
|
|
PRIMARY KEY (`migration_id`),
|
|
|
|
UNIQUE INDEX (`migration_id`)
|
|
|
|
)
|
|
|
|
');
|
2019-06-10 19:04:53 +02:00
|
|
|
} catch(PDOException $ex) {
|
2018-05-16 20:18:30 +02:00
|
|
|
$this->addError($ex);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
public function migrate(): bool {
|
2018-05-16 20:18:30 +02:00
|
|
|
$this->writeLog('Running migrations...');
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
if(!$this->createMigrationRepository()) {
|
2018-05-16 20:18:30 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$migrationScripts = $this->getMigrationScripts();
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
if(count($migrationScripts) < 1) {
|
|
|
|
if(count($this->errors) > 0) {
|
2018-05-16 20:18:30 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->writeLog('Nothing to migrate!');
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
$this->writeLog('Fetching completed migration...');
|
|
|
|
$fetchStatus = $this->targetConnection->prepare("
|
|
|
|
SELECT *, CONCAT(:basepath, '/', `migration_name`, '.php') as `migration_path`
|
|
|
|
FROM `msz_migrations`
|
|
|
|
");
|
|
|
|
$fetchStatus->bindValue('basepath', $this->migrationStorage);
|
|
|
|
$migrationStatus = $fetchStatus->execute() ? $fetchStatus->fetchAll() : [];
|
2019-06-10 19:04:53 +02:00
|
|
|
} catch(PDOException $ex) {
|
2018-05-16 20:18:30 +02:00
|
|
|
$this->addError($ex);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
if(count($migrationStatus) < 1 && count($this->errors) > 0) {
|
2018-05-16 20:18:30 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$remainingMigrations = array_diff($migrationScripts, array_column($migrationStatus, 'migration_path'));
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
if(count($remainingMigrations) < 1) {
|
2018-05-16 20:18:30 +02:00
|
|
|
$this->writeLog('Nothing to migrate!');
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$batchNumber = $this->targetConnection->query('
|
|
|
|
SELECT COALESCE(MAX(`migration_batch`), 0) + 1
|
|
|
|
FROM `msz_migrations`
|
|
|
|
')->fetchColumn();
|
|
|
|
|
|
|
|
$recordMigration = $this->targetConnection->prepare('
|
|
|
|
INSERT INTO `msz_migrations`
|
|
|
|
(`migration_name`, `migration_batch`)
|
|
|
|
VALUES
|
|
|
|
(:name, :batch)
|
|
|
|
');
|
|
|
|
$recordMigration->bindValue('batch', $batchNumber);
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
foreach($remainingMigrations as $migration) {
|
2018-05-16 20:18:30 +02:00
|
|
|
$filename = pathinfo($migration, PATHINFO_FILENAME);
|
|
|
|
$filenameSplit = explode('_', $filename);
|
|
|
|
$recordMigration->bindValue('name', $filename);
|
|
|
|
$migrationName = '';
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
if(count($filenameSplit) < 5) {
|
2018-05-16 20:18:30 +02:00
|
|
|
$this->addError(new Exception("Invalid migration name: '{$filename}'"));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
for($i = 4; $i < count($filenameSplit); $i++) {
|
2018-09-15 23:55:26 +02:00
|
|
|
$migrationName .= ucfirst(mb_strtolower($filenameSplit[$i]));
|
2018-05-16 20:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
include_once $migration;
|
|
|
|
|
|
|
|
$this->writeLog("Running migration '{$filename}'...");
|
|
|
|
$migrationFunction = sprintf(self::MIGRATION_NAMESPACE, $migrationName, 'migrate_up');
|
|
|
|
$migrationFunction($this->targetConnection);
|
|
|
|
$recordMigration->execute();
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->writeLog('Successfully completed all migrations!');
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function rollback(): bool
|
|
|
|
{
|
|
|
|
$this->writeLog('Rolling back last migration batch...');
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
if(!$this->createMigrationRepository()) {
|
2018-05-16 20:18:30 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
$fetchStatus = $this->targetConnection->prepare("
|
|
|
|
SELECT *, CONCAT(:basepath, '/', `migration_name`, '.php') as `migration_path`
|
|
|
|
FROM `msz_migrations`
|
|
|
|
WHERE `migration_batch` = (
|
|
|
|
SELECT MAX(`migration_batch`)
|
|
|
|
FROM `msz_migrations`
|
|
|
|
)
|
|
|
|
");
|
|
|
|
$fetchStatus->bindValue('basepath', $this->migrationStorage);
|
|
|
|
$migrations = $fetchStatus->execute() ? $fetchStatus->fetchAll() : [];
|
2019-06-10 19:04:53 +02:00
|
|
|
} catch(PDOException $ex) {
|
2018-05-16 20:18:30 +02:00
|
|
|
$this->addError($ex);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
if(count($migrations) < 1) {
|
|
|
|
if(count($this->errors) > 0) {
|
2018-05-16 20:18:30 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->writeLog('Nothing to roll back!');
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$migrationScripts = $this->getMigrationScripts();
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
if(count($migrationScripts) < count($migrations)) {
|
2018-05-16 20:18:30 +02:00
|
|
|
$this->addError(new Exception('There are missing migration scripts!'));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$removeRecord = $this->targetConnection->prepare('
|
|
|
|
DELETE FROM `msz_migrations`
|
|
|
|
WHERE `migration_id` = :id
|
|
|
|
');
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
foreach($migrations as $migration) {
|
|
|
|
if(!file_exists($migration['migration_path'])) {
|
2018-05-16 20:18:30 +02:00
|
|
|
$this->addError(new Exception("Migration '{$migration['migration_name']}' does not exist."));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$nameSplit = explode('_', $migration['migration_name']);
|
|
|
|
$migrationName = '';
|
|
|
|
|
2019-06-10 19:04:53 +02:00
|
|
|
for($i = 4; $i < count($nameSplit); $i++) {
|
2018-09-15 23:55:26 +02:00
|
|
|
$migrationName .= ucfirst(mb_strtolower($nameSplit[$i]));
|
2018-05-16 20:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
include_once $migration['migration_path'];
|
|
|
|
|
|
|
|
$this->writeLog("Rolling '{$migration['migration_name']}' back...");
|
|
|
|
$migrationFunction = sprintf(self::MIGRATION_NAMESPACE, $migrationName, 'migrate_down');
|
|
|
|
$migrationFunction($this->targetConnection);
|
|
|
|
|
|
|
|
$removeRecord->bindValue('id', $migration['migration_id']);
|
|
|
|
$removeRecord->execute();
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->writeLog('Successfully completed all rollbacks');
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|