1 | <?php |
||||||||||
2 | |||||||||||
3 | declare(strict_types=1); |
||||||||||
4 | |||||||||||
5 | /* |
||||||||||
6 | * This file is part of Biurad opensource projects. |
||||||||||
7 | * |
||||||||||
8 | * PHP version 7.2 and above required |
||||||||||
9 | * |
||||||||||
10 | * @author Divine Niiquaye Ibok <[email protected]> |
||||||||||
11 | * @copyright 2019 Biurad Group (https://biurad.com/) |
||||||||||
12 | * @license https://opensource.org/licenses/BSD-3-Clause License |
||||||||||
13 | * |
||||||||||
14 | * For the full copyright and license information, please view the LICENSE |
||||||||||
15 | * file that was distributed with this source code. |
||||||||||
16 | */ |
||||||||||
17 | |||||||||||
18 | namespace Biurad\Cycle; |
||||||||||
19 | |||||||||||
20 | use DateTime; |
||||||||||
21 | use DateTimeImmutable; |
||||||||||
22 | use DateTimeInterface; |
||||||||||
23 | use Spiral\Database\Database as SpiralDatabase; |
||||||||||
24 | use Spiral\Database\Table; |
||||||||||
25 | use Spiral\Migrations\Capsule; |
||||||||||
26 | use Spiral\Migrations\CapsuleInterface; |
||||||||||
27 | use Spiral\Migrations\Config\MigrationConfig; |
||||||||||
28 | use Spiral\Migrations\Exception\MigrationException; |
||||||||||
29 | use Spiral\Migrations\MigrationInterface; |
||||||||||
30 | use Spiral\Migrations\RepositoryInterface; |
||||||||||
31 | use Spiral\Migrations\State; |
||||||||||
32 | use Throwable; |
||||||||||
33 | |||||||||||
34 | final class Migrator |
||||||||||
35 | { |
||||||||||
36 | private const DB_DATE_FORMAT = 'Y-m-d H:i:s'; |
||||||||||
37 | |||||||||||
38 | private const MIGRATION_TABLE_FIELDS_LIST = [ |
||||||||||
39 | 'id', |
||||||||||
40 | 'migration', |
||||||||||
41 | 'time_executed', |
||||||||||
42 | 'created_at', |
||||||||||
43 | ]; |
||||||||||
44 | |||||||||||
45 | /** @var MigrationConfig */ |
||||||||||
46 | private $config; |
||||||||||
47 | |||||||||||
48 | /** @var Database */ |
||||||||||
49 | private $dbal; |
||||||||||
50 | |||||||||||
51 | /** @var RepositoryInterface */ |
||||||||||
52 | private $repository; |
||||||||||
53 | |||||||||||
54 | /** |
||||||||||
55 | * @param MigrationConfig $config |
||||||||||
56 | * @param Database $dbal |
||||||||||
57 | * @param RepositoryInterface $repository |
||||||||||
58 | */ |
||||||||||
59 | public function __construct( |
||||||||||
60 | MigrationConfig $config, |
||||||||||
61 | Database $dbal, |
||||||||||
62 | RepositoryInterface $repository |
||||||||||
63 | ) { |
||||||||||
64 | $this->config = $config; |
||||||||||
65 | $this->repository = $repository; |
||||||||||
66 | $this->dbal = $dbal; |
||||||||||
67 | } |
||||||||||
68 | |||||||||||
69 | /** |
||||||||||
70 | * @return MigrationConfig |
||||||||||
71 | */ |
||||||||||
72 | public function getConfig(): MigrationConfig |
||||||||||
73 | { |
||||||||||
74 | return $this->config; |
||||||||||
75 | } |
||||||||||
76 | |||||||||||
77 | /** |
||||||||||
78 | * @return RepositoryInterface |
||||||||||
79 | */ |
||||||||||
80 | public function getRepository(): RepositoryInterface |
||||||||||
81 | { |
||||||||||
82 | return $this->repository; |
||||||||||
83 | } |
||||||||||
84 | |||||||||||
85 | /** |
||||||||||
86 | * Check if all related databases are configures with migrations. |
||||||||||
87 | * |
||||||||||
88 | * @return bool |
||||||||||
89 | */ |
||||||||||
90 | public function isConfigured(): bool |
||||||||||
91 | { |
||||||||||
92 | foreach ($this->dbal->getDatabases() as $db) { |
||||||||||
93 | if (!$db->hasTable($this->config->getTable()) || !$this->checkMigrationTableStructure($db)) { |
||||||||||
94 | return false; |
||||||||||
95 | } |
||||||||||
96 | } |
||||||||||
97 | |||||||||||
98 | return !$this->isRestoreMigrationDataRequired(); |
||||||||||
99 | } |
||||||||||
100 | |||||||||||
101 | /** |
||||||||||
102 | * Configure all related databases with migration table. |
||||||||||
103 | */ |
||||||||||
104 | public function configure(): void |
||||||||||
105 | { |
||||||||||
106 | if ($this->isConfigured()) { |
||||||||||
107 | return; |
||||||||||
108 | } |
||||||||||
109 | |||||||||||
110 | foreach ($this->dbal->getDatabases() as $db) { |
||||||||||
111 | $schema = $db->table($this->config->getTable())->getSchema(); |
||||||||||
112 | |||||||||||
113 | // Schema update will automatically sync all needed data |
||||||||||
114 | $schema->primary('id'); |
||||||||||
115 | $schema->string('migration', 191)->nullable(false); |
||||||||||
116 | $schema->datetime('time_executed')->datetime(); |
||||||||||
117 | $schema->datetime('created_at')->datetime(); |
||||||||||
118 | $schema->index(['migration', 'created_at']) |
||||||||||
119 | ->unique(true); |
||||||||||
120 | |||||||||||
121 | if ($schema->hasIndex(['migration'])) { |
||||||||||
122 | $schema->dropIndex(['migration']); |
||||||||||
123 | } |
||||||||||
124 | |||||||||||
125 | $schema->save(); |
||||||||||
126 | } |
||||||||||
127 | |||||||||||
128 | if ($this->isRestoreMigrationDataRequired()) { |
||||||||||
129 | $this->restoreMigrationData(); |
||||||||||
130 | } |
||||||||||
131 | } |
||||||||||
132 | |||||||||||
133 | /** |
||||||||||
134 | * Get every available migration with valid meta information. |
||||||||||
135 | * |
||||||||||
136 | * @return MigrationInterface[] |
||||||||||
137 | */ |
||||||||||
138 | public function getMigrations(): array |
||||||||||
139 | { |
||||||||||
140 | $result = []; |
||||||||||
141 | |||||||||||
142 | foreach ($this->repository->getMigrations() as $migration) { |
||||||||||
143 | //Populating migration state and execution time (if any) |
||||||||||
144 | $result[] = $migration->withState($this->resolveState($migration)); |
||||||||||
145 | } |
||||||||||
146 | |||||||||||
147 | return $result; |
||||||||||
148 | } |
||||||||||
149 | |||||||||||
150 | /** |
||||||||||
151 | * Execute one migration and return it's instance. |
||||||||||
152 | * |
||||||||||
153 | * @param CapsuleInterface $capsule |
||||||||||
154 | * |
||||||||||
155 | * @throws MigrationException |
||||||||||
156 | * |
||||||||||
157 | * @return null|MigrationInterface |
||||||||||
158 | */ |
||||||||||
159 | public function run(CapsuleInterface $capsule = null): ?MigrationInterface |
||||||||||
160 | { |
||||||||||
161 | if (!$this->isConfigured()) { |
||||||||||
162 | throw new MigrationException('Unable to run migration, Migrator not configured'); |
||||||||||
163 | } |
||||||||||
164 | |||||||||||
165 | foreach ($this->getMigrations() as $migration) { |
||||||||||
166 | if ($migration->getState()->getStatus() !== State::STATUS_PENDING) { |
||||||||||
167 | continue; |
||||||||||
168 | } |
||||||||||
169 | |||||||||||
170 | try { |
||||||||||
171 | $capsule = $capsule ?? new Capsule($this->dbal->database($migration->getDatabase())); |
||||||||||
172 | $capsule->getDatabase($migration->getDatabase())->transaction( |
||||||||||
0 ignored issues
–
show
The call to
Spiral\Migrations\Capsule::getDatabase() has too many arguments starting with $migration->getDatabase() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. ![]() The call to
Spiral\Migrations\CapsuleInterface::getDatabase() has too many arguments starting with $migration->getDatabase() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. ![]() |
|||||||||||
173 | static function () use ($migration, $capsule): void { |
||||||||||
174 | $migration->withCapsule($capsule)->up(); |
||||||||||
175 | } |
||||||||||
176 | ); |
||||||||||
177 | |||||||||||
178 | $this->migrationTable($migration->getDatabase())->insertOne( |
||||||||||
179 | [ |
||||||||||
180 | 'migration' => $migration->getState()->getName(), |
||||||||||
181 | 'time_executed' => new DateTime('now'), |
||||||||||
182 | 'created_at' => $this->getMigrationCreatedAtForDb($migration), |
||||||||||
183 | ] |
||||||||||
184 | ); |
||||||||||
185 | |||||||||||
186 | return $migration->withState($this->resolveState($migration)); |
||||||||||
187 | } catch (Throwable $exception) { |
||||||||||
188 | throw new MigrationException( |
||||||||||
189 | \sprintf( |
||||||||||
190 | 'Error in the migration (%s) occurred: %s', |
||||||||||
191 | \sprintf( |
||||||||||
192 | '%s (%s)', |
||||||||||
193 | $migration->getState()->getName(), |
||||||||||
194 | $migration->getState()->getTimeCreated()->format(self::DB_DATE_FORMAT) |
||||||||||
195 | ), |
||||||||||
196 | $exception->getMessage() |
||||||||||
197 | ), |
||||||||||
198 | $exception->getCode(), |
||||||||||
199 | $exception |
||||||||||
200 | ); |
||||||||||
201 | } |
||||||||||
202 | } |
||||||||||
203 | |||||||||||
204 | return null; |
||||||||||
205 | } |
||||||||||
206 | |||||||||||
207 | /** |
||||||||||
208 | * Rollback last migration and return it's instance. |
||||||||||
209 | * |
||||||||||
210 | * @param CapsuleInterface $capsule |
||||||||||
211 | * |
||||||||||
212 | * @throws Throwable |
||||||||||
213 | * |
||||||||||
214 | * @return null|MigrationInterface |
||||||||||
215 | */ |
||||||||||
216 | public function rollback(CapsuleInterface $capsule = null): ?MigrationInterface |
||||||||||
217 | { |
||||||||||
218 | if (!$this->isConfigured()) { |
||||||||||
219 | throw new MigrationException('Unable to run migration, Migrator not configured'); |
||||||||||
220 | } |
||||||||||
221 | |||||||||||
222 | /** @var MigrationInterface $migration */ |
||||||||||
223 | foreach (\array_reverse($this->getMigrations()) as $migration) { |
||||||||||
224 | if ($migration->getState()->getStatus() !== State::STATUS_EXECUTED) { |
||||||||||
225 | continue; |
||||||||||
226 | } |
||||||||||
227 | |||||||||||
228 | $capsule = $capsule ?? new Capsule($this->dbal->database($migration->getDatabase())); |
||||||||||
229 | $capsule->getDatabase()->transaction( |
||||||||||
230 | static function () use ($migration, $capsule): void { |
||||||||||
231 | $migration->withCapsule($capsule)->down(); |
||||||||||
232 | } |
||||||||||
233 | ); |
||||||||||
234 | |||||||||||
235 | $migrationData = $this->fetchMigrationData($migration); |
||||||||||
236 | |||||||||||
237 | if (!empty($migrationData)) { |
||||||||||
238 | $this->migrationTable($migration->getDatabase()) |
||||||||||
239 | ->delete(['id' => $migrationData['id']]) |
||||||||||
240 | ->run(); |
||||||||||
241 | } |
||||||||||
242 | |||||||||||
243 | return $migration->withState($this->resolveState($migration)); |
||||||||||
244 | } |
||||||||||
245 | |||||||||||
246 | return null; |
||||||||||
247 | } |
||||||||||
248 | |||||||||||
249 | /** |
||||||||||
250 | * Clarify migration state with valid status and execution time |
||||||||||
251 | * |
||||||||||
252 | * @param MigrationInterface $migration |
||||||||||
253 | * |
||||||||||
254 | * @return State |
||||||||||
255 | */ |
||||||||||
256 | protected function resolveState(MigrationInterface $migration): State |
||||||||||
257 | { |
||||||||||
258 | $db = $this->dbal->database($migration->getDatabase()); |
||||||||||
259 | |||||||||||
260 | $data = $this->fetchMigrationData($migration); |
||||||||||
261 | |||||||||||
262 | if (empty($data['time_executed'])) { |
||||||||||
263 | return $migration->getState()->withStatus(State::STATUS_PENDING); |
||||||||||
264 | } |
||||||||||
265 | |||||||||||
266 | return $migration->getState()->withStatus( |
||||||||||
267 | State::STATUS_EXECUTED, |
||||||||||
268 | new DateTimeImmutable($data['time_executed'], $db->getDriver()->getTimezone()) |
||||||||||
269 | ); |
||||||||||
270 | } |
||||||||||
271 | |||||||||||
272 | /** |
||||||||||
273 | * Migration table, all migration information will be stored in it. |
||||||||||
274 | * |
||||||||||
275 | * @param null|string $database |
||||||||||
276 | * |
||||||||||
277 | * @return Table |
||||||||||
278 | */ |
||||||||||
279 | protected function migrationTable(string $database = null): Table |
||||||||||
280 | { |
||||||||||
281 | return $this->dbal->database($database)->table($this->config->getTable()); |
||||||||||
282 | } |
||||||||||
283 | |||||||||||
284 | protected function checkMigrationTableStructure(SpiralDatabase $db): bool |
||||||||||
285 | { |
||||||||||
286 | $table = $db->table($this->config->getTable()); |
||||||||||
287 | |||||||||||
288 | foreach (self::MIGRATION_TABLE_FIELDS_LIST as $field) { |
||||||||||
289 | if (!$table->hasColumn($field)) { |
||||||||||
290 | return false; |
||||||||||
291 | } |
||||||||||
292 | } |
||||||||||
293 | |||||||||||
294 | if (!$table->hasIndex(['migration', 'created_at'])) { |
||||||||||
295 | return false; |
||||||||||
296 | } |
||||||||||
297 | |||||||||||
298 | return true; |
||||||||||
299 | } |
||||||||||
300 | |||||||||||
301 | /** |
||||||||||
302 | * Fetch migration information from database |
||||||||||
303 | * |
||||||||||
304 | * @param MigrationInterface $migration |
||||||||||
305 | * |
||||||||||
306 | * @return null|array |
||||||||||
307 | */ |
||||||||||
308 | protected function fetchMigrationData(MigrationInterface $migration): ?array |
||||||||||
309 | { |
||||||||||
310 | $migrationData = $this->migrationTable($migration->getDatabase()) |
||||||||||
311 | ->select('id', 'time_executed', 'created_at') |
||||||||||
312 | ->where( |
||||||||||
313 | [ |
||||||||||
314 | 'migration' => $migration->getState()->getName(), |
||||||||||
315 | 'created_at' => $this->getMigrationCreatedAtForDb($migration)->format(self::DB_DATE_FORMAT), |
||||||||||
316 | ] |
||||||||||
317 | ) |
||||||||||
318 | ->run() |
||||||||||
319 | ->fetch(); |
||||||||||
320 | |||||||||||
321 | return \is_array($migrationData) ? $migrationData : []; |
||||||||||
322 | } |
||||||||||
323 | |||||||||||
324 | protected function restoreMigrationData(): void |
||||||||||
325 | { |
||||||||||
326 | foreach ($this->repository->getMigrations() as $migration) { |
||||||||||
327 | $migrationData = $this->migrationTable($migration->getDatabase()) |
||||||||||
328 | ->select('id') |
||||||||||
329 | ->where( |
||||||||||
330 | [ |
||||||||||
331 | 'migration' => $migration->getState()->getName(), |
||||||||||
332 | 'created_at' => null, |
||||||||||
333 | ] |
||||||||||
334 | ) |
||||||||||
335 | ->run() |
||||||||||
336 | ->fetch(); |
||||||||||
337 | |||||||||||
338 | if (!empty($migrationData)) { |
||||||||||
339 | $this->migrationTable($migration->getDatabase()) |
||||||||||
340 | ->update( |
||||||||||
341 | ['created_at' => $this->getMigrationCreatedAtForDb($migration)], |
||||||||||
342 | ['id' => $migrationData['id']] |
||||||||||
343 | ) |
||||||||||
344 | ->run(); |
||||||||||
345 | } |
||||||||||
346 | } |
||||||||||
347 | } |
||||||||||
348 | |||||||||||
349 | /** |
||||||||||
350 | * Check if some data modification required |
||||||||||
351 | * |
||||||||||
352 | * @return bool |
||||||||||
353 | */ |
||||||||||
354 | protected function isRestoreMigrationDataRequired(): bool |
||||||||||
355 | { |
||||||||||
356 | foreach ($this->dbal->getDatabases() as $db) { |
||||||||||
357 | $table = $db->table($this->config->getTable()); |
||||||||||
358 | |||||||||||
359 | if ( |
||||||||||
360 | $table->select('id') |
||||||||||
361 | ->where(['created_at' => null]) |
||||||||||
362 | ->count() > 0 |
||||||||||
363 | ) { |
||||||||||
364 | return true; |
||||||||||
365 | } |
||||||||||
366 | } |
||||||||||
367 | |||||||||||
368 | return false; |
||||||||||
369 | } |
||||||||||
370 | |||||||||||
371 | protected function getMigrationCreatedAtForDb(MigrationInterface $migration): DateTimeInterface |
||||||||||
372 | { |
||||||||||
373 | $db = $this->dbal->database($migration->getDatabase()); |
||||||||||
374 | |||||||||||
375 | return DateTimeImmutable::createFromFormat( |
||||||||||
0 ignored issues
–
show
|
|||||||||||
376 | self::DB_DATE_FORMAT, |
||||||||||
377 | $migration->getState()->getTimeCreated()->format(self::DB_DATE_FORMAT), |
||||||||||
378 | $db->getDriver()->getTimezone() |
||||||||||
379 | ); |
||||||||||
380 | } |
||||||||||
381 | } |
||||||||||
382 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.