icingaweb2/library/Icinga/Application/MigrationManager.php

247 lines
6.0 KiB
PHP

<?php
/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
namespace Icinga\Application;
use Countable;
use Generator;
use Icinga\Application\Hook\MigrationHook;
use Icinga\Exception\NotFoundError;
use ipl\I18n\Translation;
/**
* Migration manager encapsulates PHP code and DB migrations and manages all pending migrations in a
* structured way.
*/
final class MigrationManager implements Countable
{
use Translation;
/** @var array<string, MigrationHook> All pending migration hooks */
protected $pendingMigrations;
/** @var MigrationManager */
private static $instance;
private function __construct()
{
}
/**
* Get the instance of this manager
*
* @return $this
*/
public static function instance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get all pending migrations
*
* @return array<string, MigrationHook>
*/
public function getPendingMigrations(): array
{
if ($this->pendingMigrations === null) {
$this->load();
}
return $this->pendingMigrations;
}
/**
* Get whether there are any pending migrations
*
* @return bool
*/
public function hasPendingMigrations(): bool
{
return $this->count() > 0;
}
public function hasMigrations(string $module): bool
{
if (! $this->hasPendingMigrations()) {
return false;
}
return isset($this->getPendingMigrations()[$module]);
}
/**
* Get pending migration matching the given module name
*
* @param string $module
*
* @return MigrationHook
*
* @throws NotFoundError When there are no pending PHP code migrations matching the given module name
*/
public function getMigration(string $module): MigrationHook
{
if (! $this->hasMigrations($module)) {
throw new NotFoundError('There are no pending migrations matching the given name: %s', $module);
}
return $this->getPendingMigrations()[$module];
}
/**
* Get the number of all pending migrations
*
* @return int
*/
public function count(): int
{
return count($this->getPendingMigrations());
}
/**
* Apply all pending migrations matching the given migration module name
*
* @param string $module
*
* @return bool
*/
public function applyByName(string $module): bool
{
$migration = $this->getMigration($module);
if ($migration->isModule() && $this->hasMigrations(MigrationHook::DEFAULT_MODULE)) {
return false;
}
return $this->apply($migration);
}
/**
* Apply the given migration hook
*
* @param MigrationHook $hook
*
* @return bool
*/
public function apply(MigrationHook $hook): bool
{
if ($hook->isModule() && $this->hasMigrations(MigrationHook::DEFAULT_MODULE)) {
Logger::error(
'Please apply the Icinga Web pending migration(s) first or apply all the migrations instead'
);
return false;
}
if ($hook->run()) {
unset($this->pendingMigrations[$hook->getModuleName()]);
Logger::info('Applied pending %s migrations successfully', $hook->getName());
return true;
}
return false;
}
/**
* Apply all pending modules/framework migrations
*
* @return bool
*/
public function applyAll(): bool
{
$default = MigrationHook::DEFAULT_MODULE;
if ($this->hasMigrations($default)) {
$migration = $this->getMigration($default);
if (! $this->apply($migration)) {
return false;
}
}
$succeeded = true;
foreach ($this->getPendingMigrations() as $migration) {
if (! $this->apply($migration) && $succeeded) {
$succeeded = false;
}
}
return $succeeded;
}
/**
* Yield module and framework pending migrations separately
*
* @param bool $modules
*
* @return Generator<MigrationHook>
*/
public function yieldMigrations(bool $modules = false): Generator
{
foreach ($this->getPendingMigrations() as $migration) {
if ($modules === $migration->isModule()) {
yield $migration;
}
}
}
protected function load(): void
{
$this->pendingMigrations = [];
/** @var MigrationHook $hook */
foreach (Hook::all('migration') as $hook) {
if (empty($hook->getMigrations())) {
continue;
}
$this->pendingMigrations[$hook->getModuleName()] = $hook;
}
ksort($this->pendingMigrations);
}
/**
* Get all pending migrations as an array
*
* @return array<string, mixed>
*/
public function toArray(): array
{
$framework = [];
$serialize = function (MigrationHook $hook): array {
$serialized = [
'name' => $hook->getName(),
'module' => $hook->getModuleName(),
'isModule' => $hook->isModule(),
'migrated_version' => $hook->getVersion(),
'migrations' => []
];
foreach ($hook->getMigrations() as $migration) {
$serialized['migrations'][$migration->getVersion()] = [
'path' => $migration->getScriptPath(),
'error' => $migration->getLastState()
];
}
return $serialized;
};
foreach ($this->yieldMigrations() as $migration) {
$framework[] = $serialize($migration);
}
$modules = [];
foreach ($this->yieldMigrations(true) as $migration) {
$modules[] = $serialize($migration);
}
return ['System' => $framework, 'Modules' => $modules];
}
}