Passed
Push — master ( aa7235...10a192 )
by Aleksei
03:52 queued 01:45
created

Migrator::getMigrations()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 9
rs 10
cc 2
nc 2
nop 0
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license MIT
7
 * @author  Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Migrations;
13
14
use Spiral\Database\Database;
15
use Spiral\Database\DatabaseManager;
16
use Spiral\Database\Table;
17
use Spiral\Migrations\Config\MigrationConfig;
18
use Spiral\Migrations\Exception\MigrationException;
19
20
final class Migrator
21
{
22
    private const DB_DATE_FORMAT = 'Y-m-d H:i:s';
23
24
    private const MIGRATION_TABLE_FIELDS_LIST = [
25
        'id',
26
        'migration',
27
        'time_executed',
28
        'created_at'
29
    ];
30
31
    /** @var MigrationConfig */
32
    private $config;
33
34
    /** @var DatabaseManager */
35
    private $dbal;
36
37
    /** @var RepositoryInterface */
38
    private $repository;
39
40
    /**
41
     * @param MigrationConfig     $config
42
     * @param DatabaseManager     $dbal
43
     * @param RepositoryInterface $repository
44
     */
45
    public function __construct(
46
        MigrationConfig $config,
47
        DatabaseManager $dbal,
48
        RepositoryInterface $repository
49
    ) {
50
        $this->config = $config;
51
        $this->repository = $repository;
52
        $this->dbal = $dbal;
53
    }
54
55
    /**
56
     * @return MigrationConfig
57
     */
58
    public function getConfig(): MigrationConfig
59
    {
60
        return $this->config;
61
    }
62
63
    /**
64
     * @return RepositoryInterface
65
     */
66
    public function getRepository(): RepositoryInterface
67
    {
68
        return $this->repository;
69
    }
70
71
    /**
72
     * Check if all related databases are configures with migrations.
73
     *
74
     * @return bool
75
     */
76
    public function isConfigured(): bool
77
    {
78
        foreach ($this->dbal->getDatabases() as $db) {
79
            if (!$db->hasTable($this->config->getTable()) || !$this->checkMigrationTableStructure($db)) {
80
                return false;
81
            }
82
        }
83
84
        return !$this->isRestoreMigrationDataRequired();
85
    }
86
87
    /**
88
     * Configure all related databases with migration table.
89
     */
90
    public function configure(): void
91
    {
92
        if ($this->isConfigured()) {
93
            return;
94
        }
95
96
        foreach ($this->dbal->getDatabases() as $db) {
97
            $schema = $db->table($this->config->getTable())->getSchema();
98
99
            // Schema update will automatically sync all needed data
100
            $schema->primary('id');
101
            $schema->string('migration', 191)->nullable(false);
102
            $schema->datetime('time_executed')->datetime();
103
            $schema->datetime('created_at')->datetime();
104
            $schema->index(['migration', 'created_at'])
105
                ->unique(true);
106
107
            if ($schema->hasIndex(['migration'])) {
108
                $schema->dropIndex(['migration']);
109
            }
110
111
            $schema->save();
112
        }
113
114
        if ($this->isRestoreMigrationDataRequired()) {
115
            $this->restoreMigrationData();
116
        }
117
    }
118
119
    /**
120
     * Get every available migration with valid meta information.
121
     *
122
     * @return MigrationInterface[]
123
     */
124
    public function getMigrations(): array
125
    {
126
        $result = [];
127
        foreach ($this->repository->getMigrations() as $migration) {
128
            //Populating migration state and execution time (if any)
129
            $result[] = $migration->withState($this->resolveState($migration));
130
        }
131
132
        return $result;
133
    }
134
135
    /**
136
     * Execute one migration and return it's instance.
137
     *
138
     * @param CapsuleInterface $capsule
139
     *
140
     * @return null|MigrationInterface
141
     *
142
     * @throws MigrationException
143
     */
144
    public function run(CapsuleInterface $capsule = null): ?MigrationInterface
145
    {
146
        if (!$this->isConfigured()) {
147
            throw new MigrationException('Unable to run migration, Migrator not configured');
148
        }
149
150
        foreach ($this->getMigrations() as $migration) {
151
            if ($migration->getState()->getStatus() !== State::STATUS_PENDING) {
152
                continue;
153
            }
154
155
            try {
156
                $capsule = $capsule ?? new Capsule($this->dbal->database($migration->getDatabase()));
157
                $capsule->getDatabase($migration->getDatabase())->transaction(
0 ignored issues
show
Unused Code introduced by
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 ignore-call  annotation

157
                $capsule->/** @scrutinizer ignore-call */ 
158
                          getDatabase($migration->getDatabase())->transaction(

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.

Loading history...
Unused Code introduced by
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 ignore-call  annotation

157
                $capsule->/** @scrutinizer ignore-call */ 
158
                          getDatabase($migration->getDatabase())->transaction(

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.

Loading history...
Bug introduced by
The method transaction() does not exist on Spiral\Database\DatabaseManager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

157
                $capsule->getDatabase($migration->getDatabase())->/** @scrutinizer ignore-call */ transaction(

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.

Loading history...
158
                    static function () use ($migration, $capsule): void {
159
                        $migration->withCapsule($capsule)->up();
160
                    }
161
                );
162
163
                $this->migrationTable($migration->getDatabase())->insertOne(
164
                    [
165
                        'migration' => $migration->getState()->getName(),
166
                        'time_executed' => new \DateTime('now'),
167
                        'created_at' => $this->getMigrationCreatedAtForDb($migration),
168
                    ]
169
                );
170
171
                return $migration->withState($this->resolveState($migration));
172
            } catch (\Throwable $exception) {
173
                throw new MigrationException(
174
                    \sprintf(
175
                        'Error in the migration (%s) occurred: %s',
176
                        \sprintf(
177
                            '%s (%s)',
178
                            $migration->getState()->getName(),
179
                            $migration->getState()->getTimeCreated()->format(self::DB_DATE_FORMAT)
180
                        ),
181
                        $exception->getMessage()
182
                    ),
183
                    $exception->getCode(),
184
                    $exception
185
                );
186
            }
187
        }
188
189
        return null;
190
    }
191
192
    /**
193
     * Rollback last migration and return it's instance.
194
     *
195
     * @param CapsuleInterface $capsule
196
     * @return null|MigrationInterface
197
     *
198
     * @throws \Throwable
199
     */
200
    public function rollback(CapsuleInterface $capsule = null): ?MigrationInterface
201
    {
202
        if (!$this->isConfigured()) {
203
            throw new MigrationException('Unable to run migration, Migrator not configured');
204
        }
205
206
        /** @var MigrationInterface $migration */
207
        foreach (array_reverse($this->getMigrations()) as $migration) {
208
            if ($migration->getState()->getStatus() !== State::STATUS_EXECUTED) {
209
                continue;
210
            }
211
212
            $capsule = $capsule ?? new Capsule($this->dbal->database($migration->getDatabase()));
213
            $capsule->getDatabase()->transaction(
214
                static function () use ($migration, $capsule): void {
215
                    $migration->withCapsule($capsule)->down();
216
                }
217
            );
218
219
            $migrationData = $this->fetchMigrationData($migration);
220
221
            if (!empty($migrationData)) {
222
                $this->migrationTable($migration->getDatabase())
223
                    ->delete(['id' => $migrationData['id']])
224
                    ->run();
225
            }
226
227
            return $migration->withState($this->resolveState($migration));
228
        }
229
230
        return null;
231
    }
232
233
    /**
234
     * Clarify migration state with valid status and execution time
235
     *
236
     * @param MigrationInterface $migration
237
     * @return State
238
     */
239
    protected function resolveState(MigrationInterface $migration): State
240
    {
241
        $db = $this->dbal->database($migration->getDatabase());
242
243
        $data = $this->fetchMigrationData($migration);
244
245
        if (empty($data['time_executed'])) {
246
            return $migration->getState()->withStatus(State::STATUS_PENDING);
247
        }
248
249
        return $migration->getState()->withStatus(
250
            State::STATUS_EXECUTED,
251
            new \DateTimeImmutable($data['time_executed'], $db->getDriver()->getTimezone())
252
        );
253
    }
254
255
    /**
256
     * Migration table, all migration information will be stored in it.
257
     *
258
     * @param string|null $database
259
     * @return Table
260
     */
261
    protected function migrationTable(string $database = null): Table
262
    {
263
        return $this->dbal->database($database)->table($this->config->getTable());
264
    }
265
266
    protected function checkMigrationTableStructure(Database $db): bool
267
    {
268
        $table = $db->table($this->config->getTable());
269
270
        foreach (self::MIGRATION_TABLE_FIELDS_LIST as $field) {
271
            if (!$table->hasColumn($field)) {
272
                return false;
273
            }
274
        }
275
276
        if (!$table->hasIndex(['migration', 'created_at'])) {
277
            return false;
278
        }
279
280
        return true;
281
    }
282
283
    /**
284
     * Fetch migration information from database
285
     *
286
     * @param MigrationInterface $migration
287
     *
288
     * @return array|null
289
     */
290
    protected function fetchMigrationData(MigrationInterface $migration): ?array
291
    {
292
        $migrationData = $this->migrationTable($migration->getDatabase())
293
            ->select('id', 'time_executed', 'created_at')
294
            ->where(
295
                [
296
                    'migration' => $migration->getState()->getName(),
297
                    'created_at' => $this->getMigrationCreatedAtForDb($migration)->format(self::DB_DATE_FORMAT),
298
                ]
299
            )
300
            ->run()
301
            ->fetch();
302
303
        return is_array($migrationData) ? $migrationData : [];
304
    }
305
306
    protected function restoreMigrationData(): void
307
    {
308
        foreach ($this->repository->getMigrations() as $migration) {
309
            $migrationData = $this->migrationTable($migration->getDatabase())
310
                ->select('id')
311
                ->where(
312
                    [
313
                        'migration' => $migration->getState()->getName(),
314
                        'created_at' => null,
315
                    ]
316
                )
317
                ->run()
318
                ->fetch();
319
320
            if (!empty($migrationData)) {
321
                $this->migrationTable($migration->getDatabase())
322
                    ->update(
323
                        ['created_at' => $this->getMigrationCreatedAtForDb($migration)],
324
                        ['id' => $migrationData['id']]
325
                    )
326
                    ->run();
327
            }
328
        }
329
    }
330
331
    /**
332
     * Check if some data modification required
333
     *
334
     * @return bool
335
     */
336
    protected function isRestoreMigrationDataRequired(): bool
337
    {
338
        foreach ($this->dbal->getDatabases() as $db) {
339
            $table = $db->table($this->config->getTable());
340
341
            if (
342
                $table->select('id')
343
                    ->where(['created_at' => null])
344
                    ->count() > 0
345
            ) {
346
                return true;
347
            }
348
        }
349
350
        return false;
351
    }
352
353
    protected function getMigrationCreatedAtForDb(MigrationInterface $migration): \DateTimeInterface
354
    {
355
        $db = $this->dbal->database($migration->getDatabase());
356
357
        return \DateTimeImmutable::createFromFormat(
358
            self::DB_DATE_FORMAT,
359
            $migration->getState()->getTimeCreated()->format(self::DB_DATE_FORMAT),
360
            $db->getDriver()->getTimezone()
361
        );
362
    }
363
}
364