173 lines
5.5 KiB
PHP
173 lines
5.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App;
|
|
|
|
final class ModuleMigrationService
|
|
{
|
|
public function __construct(
|
|
private \PDO $basePdo,
|
|
private ModuleManager $modules
|
|
) {}
|
|
|
|
public function status(array $module): array
|
|
{
|
|
$migrations = $this->discover($module);
|
|
$applied = $this->applied((string)$module['name']);
|
|
$pending = [];
|
|
$changed = [];
|
|
|
|
foreach ($migrations as $migration) {
|
|
$id = $migration['id'];
|
|
if (!isset($applied[$id])) {
|
|
$pending[] = $migration;
|
|
continue;
|
|
}
|
|
if (($applied[$id]['checksum'] ?? '') !== $migration['checksum']) {
|
|
$changed[] = $migration;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'available' => count($migrations),
|
|
'applied' => count($applied),
|
|
'pending' => $pending,
|
|
'changed' => $changed,
|
|
];
|
|
}
|
|
|
|
public function applyPending(array $module): array
|
|
{
|
|
$status = $this->status($module);
|
|
$applied = [];
|
|
$moduleName = (string)$module['name'];
|
|
$context = new ModuleMigrationContext($this->basePdo, $this->modules, $module);
|
|
|
|
foreach ($status['pending'] as $migration) {
|
|
$this->basePdo->beginTransaction();
|
|
try {
|
|
$runner = require $migration['path'];
|
|
if (is_object($runner) && method_exists($runner, 'up')) {
|
|
$runner->up($context);
|
|
} elseif (is_callable($runner)) {
|
|
$runner($context);
|
|
} else {
|
|
throw new \RuntimeException('Migration does not return a callable or object with up().');
|
|
}
|
|
|
|
$this->record($moduleName, $migration);
|
|
$this->basePdo->commit();
|
|
$applied[] = $migration;
|
|
} catch (\Throwable $e) {
|
|
if ($this->basePdo->inTransaction()) {
|
|
$this->basePdo->rollBack();
|
|
}
|
|
throw new \RuntimeException(
|
|
'Migration fehlgeschlagen: ' . $moduleName . '/' . $migration['id'] . ' - ' . $e->getMessage(),
|
|
0,
|
|
$e
|
|
);
|
|
}
|
|
}
|
|
|
|
if ($applied !== []) {
|
|
$this->recordModuleVersion($moduleName, (string)($module['version'] ?? ''));
|
|
}
|
|
|
|
return $applied;
|
|
}
|
|
|
|
private function discover(array $module): array
|
|
{
|
|
$path = (string)($module['path'] ?? '') . '/migrations';
|
|
if (!is_dir($path)) {
|
|
return [];
|
|
}
|
|
|
|
$items = [];
|
|
foreach (glob($path . '/*.php') ?: [] as $file) {
|
|
$id = basename($file, '.php');
|
|
$items[] = [
|
|
'id' => $id,
|
|
'version' => $this->versionFromId($id),
|
|
'path' => $file,
|
|
'checksum' => hash_file('sha256', $file) ?: '',
|
|
];
|
|
}
|
|
|
|
usort($items, static fn(array $a, array $b): int => strnatcmp($a['id'], $b['id']));
|
|
return $items;
|
|
}
|
|
|
|
private function applied(string $moduleName): array
|
|
{
|
|
$stmt = $this->basePdo->prepare(
|
|
"SELECT migration, version, checksum, applied_at
|
|
FROM nexus_module_migrations
|
|
WHERE module_name = :module
|
|
ORDER BY migration ASC"
|
|
);
|
|
$stmt->execute(['module' => $moduleName]);
|
|
|
|
$items = [];
|
|
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
|
|
$items[(string)$row['migration']] = $row;
|
|
}
|
|
return $items;
|
|
}
|
|
|
|
private function record(string $moduleName, array $migration): void
|
|
{
|
|
$driver = (string)$this->basePdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
|
|
if ($driver === 'pgsql' || $driver === 'sqlite') {
|
|
$sql = "INSERT INTO nexus_module_migrations (module_name, migration, version, checksum, applied_at)
|
|
VALUES (:module, :migration, :version, :checksum, CURRENT_TIMESTAMP)
|
|
ON CONFLICT(module_name, migration) DO UPDATE SET
|
|
version = excluded.version,
|
|
checksum = excluded.checksum,
|
|
applied_at = CURRENT_TIMESTAMP";
|
|
} else {
|
|
$sql = "INSERT INTO nexus_module_migrations (module_name, migration, version, checksum, applied_at)
|
|
VALUES (:module, :migration, :version, :checksum, CURRENT_TIMESTAMP)
|
|
ON DUPLICATE KEY UPDATE
|
|
version = VALUES(version),
|
|
checksum = VALUES(checksum),
|
|
applied_at = CURRENT_TIMESTAMP";
|
|
}
|
|
|
|
$stmt = $this->basePdo->prepare($sql);
|
|
$stmt->execute([
|
|
'module' => $moduleName,
|
|
'migration' => $migration['id'],
|
|
'version' => $migration['version'],
|
|
'checksum' => $migration['checksum'],
|
|
]);
|
|
}
|
|
|
|
private function recordModuleVersion(string $moduleName, string $version): void
|
|
{
|
|
if ($version === '') {
|
|
return;
|
|
}
|
|
|
|
$stmt = $this->basePdo->prepare(
|
|
"UPDATE nexus_modules
|
|
SET version = :version,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE name = :module"
|
|
);
|
|
$stmt->execute([
|
|
'module' => $moduleName,
|
|
'version' => $version,
|
|
]);
|
|
}
|
|
|
|
private function versionFromId(string $id): string
|
|
{
|
|
if (preg_match('/(?:^|_)(\d+\.\d+\.\d+)(?:_|$)/', $id, $m)) {
|
|
return $m[1];
|
|
}
|
|
return '';
|
|
}
|
|
}
|