index/src/Data/Migration/DbMigrationManager.php

149 lines
5.1 KiB
PHP

<?php
// DbMigrationManager.php
// Created: 2023-01-07
// Updated: 2023-01-07
namespace Index\Data\Migration;
use stdClass;
use InvalidArgumentException;
use DateTimeInterface;
use Index\DateTime;
use Index\Data\IDbConnection;
use Index\Data\IDbStatement;
use Index\Data\DbType;
use Index\Data\SQLite\SQLiteConnection;
class DbMigrationManager {
public const DEFAULT_TABLE = 'ndx_migrations';
private const CREATE_TRACK_TABLE = 'CREATE TABLE IF NOT EXISTS %1$s (migration_name %2$s PRIMARY KEY, migration_completed %2$s NOT NULL);';
private const CREATE_TRACK_INDEX = 'CREATE INDEX IF NOT EXISTS %1$s_completed_index ON %1$s (migration_completed);';
private const DESTROY_TRACK_TABLE = 'DROP TABLE IF EXISTS %s;';
private const CHECK_STMT = 'SELECT migration_completed IS NOT NULL FROM %s WHERE migration_name = ?;';
private const INSERT_STMT = 'INSERT INTO %s (migration_name, migration_completed) VALUES (?, ?);';
private const TEMPLATE = <<<EOF
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
final class %s implements IDbMigration {
public function migrate(IDbConnection \$conn): void {
\$conn->execute('CREATE TABLE ...');
}
}
EOF;
private IDbStatement $checkStmt;
private IDbStatement $insertStmt;
public function __construct(
private IDbConnection $conn,
private string $tableName = self::DEFAULT_TABLE,
) {}
public function createTrackingTable(): void {
// this is not ok but it works for now, there should probably be a generic type bag alias thing
$nameType = $this->conn instanceof SQLiteConnection ? 'TEXT' : 'VARCHAR(255)';
$this->conn->execute(sprintf(self::CREATE_TRACK_TABLE, $this->tableName, $nameType));
$this->conn->execute(sprintf(self::CREATE_TRACK_INDEX, $this->tableName));
}
public function destroyTrackingTable(): void {
$this->conn->execute(sprintf(self::DESTROY_TRACK_TABLE, $this->tableName));
}
public function prepareStatements(): void {
$this->checkStmt = $this->conn->prepare(sprintf(self::CHECK_STMT, $this->tableName));
$this->insertStmt = $this->conn->prepare(sprintf(self::INSERT_STMT, $this->tableName));
}
public function init(): void {
$this->createTrackingTable();
$this->prepareStatements();
}
public function checkMigration(string $name): bool {
$this->checkStmt->reset();
$this->checkStmt->addParameter(1, $name, DbType::STRING);
$this->checkStmt->execute();
$result = $this->checkStmt->getResult();
return $result->next() && !$result->isNull(0);
}
public function completeMigration(string $name, ?DateTimeInterface $dateTime = null): void {
$dateTime = ($dateTime ?? DateTime::utcNow())->format(DateTimeInterface::ATOM);
$this->insertStmt->reset();
$this->insertStmt->addParameter(1, $name, DbType::STRING);
$this->insertStmt->addParameter(2, $dateTime, DbType::STRING);
$this->insertStmt->execute();
}
public function template(string $name): string {
return sprintf(self::TEMPLATE, $name);
}
public function createFileName(string $name, ?DateTimeInterface $dateTime = null): string {
$dateTime ??= DateTime::utcNow();
if(empty($name))
throw new InvalidArgumentException('$name may not be empty.');
$name = str_replace(' ', '_', strtolower($name));
if(!preg_match('#^([a-z_]+)$#', $name))
throw new InvalidArgumentException('$name may only contain alphabetical, spaces and _ characters.');
return $dateTime->format('Y_m_d_His_') . trim($name, '_');
}
public function createClassName(string $name, ?DateTimeInterface $dateTime = null): string {
$dateTime ??= DateTime::utcNow();
if(empty($name))
throw new InvalidArgumentException('$name may not be empty.');
$name = str_replace(' ', '_', strtolower($name));
if(!preg_match('#^([a-z_]+)$#', $name))
throw new InvalidArgumentException('$name may only contain alphabetical, spaces and _ characters.');
$parts = explode('_', trim($name, '_'));
$name = '';
foreach($parts as $part)
$name .= ucfirst($part);
return $name . $dateTime->format('_Ymd_His');
}
public function createNames(string $baseName, ?DateTimeInterface $dateTime = null): object {
$dateTime ??= DateTime::utcNow();
$names = new stdClass;
$names->name = $this->createFileName($baseName, $dateTime);
$names->className = $this->createClassName($baseName, $dateTime);
return $names;
}
public function processMigrations(IDbMigrationRepo $migrations): array {
$migrations = $migrations->getMigrations();
$completed = [];
foreach($migrations as $migration) {
$name = $migration->getName();
if($this->checkMigration($name))
continue;
$migration->migrate($this->conn);
$this->completeMigration($name);
$completed[] = $name;
}
return $completed;
}
}