Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.15% covered (success)
96.15%
50 / 52
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
MigrationRegistry
96.15% covered (success)
96.15%
50 / 52
90.00% covered (success)
90.00%
9 / 10
19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 registerDefaultMigrations
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 getMigrations
83.33% covered (success)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
5.12
 getMigration
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getVersions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPendingMigrations
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getUnappliedMigrations
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getLatestVersion
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 hasMigration
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Discovers and orders migrations.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2023-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2026-01-25
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Setup\Migration;
21
22use phpMyFAQ\Configuration;
23
24class MigrationRegistry
25{
26    /** @var array<string, class-string> */
27    private array $migrationClasses = [];
28
29    /** @var array<string, MigrationInterface>|null */
30    private ?array $migrations = null;
31
32    public function __construct(
33        private readonly Configuration $configuration,
34    ) {
35        $this->registerDefaultMigrations();
36    }
37
38    /**
39     * Registers a migration class.
40     *
41     * @param class-string<MigrationInterface> $className
42     */
43    public function register(string $version, string $className): self
44    {
45        $this->migrationClasses[$version] = $className;
46        $this->migrations = null; // Reset cache
47        return $this;
48    }
49
50    /**
51     * Registers all migrations in the Versions directory.
52     */
53    private function registerDefaultMigrations(): void
54    {
55        $this->migrationClasses = [
56            '3.2.0-alpha' => Versions\Migration320Alpha::class,
57            '3.2.0-beta' => Versions\Migration320Beta::class,
58            '3.2.0-beta.2' => Versions\Migration320Beta2::class,
59            '3.2.0-RC' => Versions\Migration320RC::class,
60            '3.2.3' => Versions\Migration323::class,
61            '4.0.0-alpha' => Versions\Migration400Alpha::class,
62            '4.0.0-alpha.2' => Versions\Migration400Alpha2::class,
63            '4.0.0-alpha.3' => Versions\Migration400Alpha3::class,
64            '4.0.0-beta.2' => Versions\Migration400Beta2::class,
65            '4.0.5' => Versions\Migration405::class,
66            '4.0.7' => Versions\Migration407::class,
67            '4.0.9' => Versions\Migration409::class,
68            '4.1.0-alpha' => Versions\Migration410Alpha::class,
69            '4.1.0-alpha.2' => Versions\Migration410Alpha2::class,
70            '4.1.0-alpha.3' => Versions\Migration410Alpha3::class,
71            '4.2.0-alpha' => Versions\Migration420Alpha::class,
72        ];
73    }
74
75    /**
76     * Returns all registered migrations, sorted by version.
77     *
78     * @return array<string, MigrationInterface>
79     */
80    public function getMigrations(): array
81    {
82        if ($this->migrations !== null) {
83            return $this->migrations;
84        }
85
86        $this->migrations = [];
87
88        foreach ($this->migrationClasses as $version => $className) {
89            if (!class_exists($className)) {
90                continue;
91            }
92
93            /* @mago-expect analysis:unknown-class-instantiation - the registry instantiates version-keyed migration classes dynamically */
94            $migration = new $className($this->configuration);
95            if (!$migration instanceof MigrationInterface) {
96                continue;
97            }
98
99            $this->migrations[$version] = $migration;
100        }
101
102        // Sort by version
103        uksort($this->migrations, callback: 'version_compare');
104
105        return $this->migrations;
106    }
107
108    /**
109     * Returns a specific migration by version.
110     */
111    public function getMigration(string $version): ?MigrationInterface
112    {
113        $migrations = $this->getMigrations();
114        return $migrations[$version] ?? null;
115    }
116
117    /**
118     * Returns all versions in order.
119     *
120     * @return string[]
121     */
122    public function getVersions(): array
123    {
124        return array_keys($this->getMigrations());
125    }
126
127    /**
128     * Returns migrations that need to be applied to get from $currentVersion to the latest.
129     *
130     * @return array<string, MigrationInterface>
131     */
132    public function getPendingMigrations(string $currentVersion): array
133    {
134        $pending = [];
135
136        foreach ($this->getMigrations() as $version => $migration) {
137            if (!version_compare(version1: $currentVersion, version2: $version, operator: '<')) {
138                continue;
139            }
140
141            $pending[$version] = $migration;
142        }
143
144        return $pending;
145    }
146
147    /**
148     * Returns migrations that need to be applied based on what's already tracked.
149     *
150     * @param string[] $appliedVersions
151     * @return MigrationInterface[]
152     */
153    public function getUnappliedMigrations(array $appliedVersions): array
154    {
155        $unapplied = [];
156
157        foreach ($this->getMigrations() as $version => $migration) {
158            if (in_array($version, $appliedVersions, strict: true)) {
159                continue;
160            }
161
162            $unapplied[$version] = $migration;
163        }
164
165        return $unapplied;
166    }
167
168    /**
169     * Returns the latest migration version.
170     */
171    public function getLatestVersion(): ?string
172    {
173        $versions = $this->getVersions();
174        return $versions !== [] ? end($versions) : null;
175    }
176
177    /**
178     * Checks if a migration version exists.
179     */
180    public function hasMigration(string $version): bool
181    {
182        return array_key_exists($version, $this->migrationClasses);
183    }
184}