GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Migrator::loadMigrations()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 9
cts 9
cp 1
rs 9.7333
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2
1
<?php
2
/**
3
 * Starlit Db.
4
 *
5
 * @copyright Copyright (c) 2016 Starweb AB
6
 * @license   BSD 3-Clause
7
 */
8
9
namespace Starlit\Db\Migration;
10
11
use Starlit\Db\Db;
12
13
/**
14
 * Class for handling migration between different database versions.
15
 *
16
 * @author Andreas Nilsson <http://github.com/jandreasn>
17
 */
18
class Migrator
19
{
20
    /**
21
     * @const string
22
     */
23
    const DIRECTION_UP = 'up';
24
25
    /**
26
     * @const string
27
     */
28
    const DIRECTION_DOWN = 'down';
29
30
    /**
31
     * @var string
32
     */
33
    protected $migrationsTableName = 'migrations';
34
35
    /**
36
     * @var string
37
     */
38
    protected $migrationsDirectory;
39
40
    /**
41
     * @var Db
42
     */
43
    protected $db;
44
45
    /**
46
     * @var callable
47
     */
48
    protected $infoCallback;
49
50
    /**
51
     * @var AbstractMigration[]
52
     */
53
    protected $migrations;
54
55
    /**
56
     * @var int[]
57
     */
58
    protected $migratedNumbers;
59
60
    /**
61
     * @var bool
62
     */
63
    private $hasMigrationsTable;
64
65
    /**
66
     * @param string          $migrationsDirectory
67
     * @param Db              $db
68
     * @param callable        $infoCallback
69
     */
70 10
    public function __construct(
71
        $migrationsDirectory,
72
        Db $db,
73
        callable $infoCallback = null
74
    ) {
75 10
        $this->migrationsDirectory = $migrationsDirectory;
76 10
        $this->db = $db;
77 10
        $this->infoCallback = $infoCallback;
78 10
    }
79
80
    /**
81
     * @return \SplFileInfo[]
82
     */
83 8
    protected function findMigrationFiles()
84
    {
85 8
        $migrationFiles = [];
86 8
        $directoryIterator = new \FilesystemIterator($this->migrationsDirectory);
87 8
        foreach ($directoryIterator as $fileInfo) {
88 8
            if ($fileInfo->isFile() && $fileInfo->getExtension() === 'php') {
89 8
                $migrationFiles[] = $fileInfo;
90
            }
91
        }
92
93 8
        return $migrationFiles;
94
    }
95
96
    /**
97
     * @return AbstractMigration[]
98
     */
99 8
    protected function loadMigrations()
100
    {
101 8
        $migrations = [];
102 8
        foreach ($this->findMigrationFiles() as $file) {
103 8
            require_once $file->getPathname();
104
105 8
            $className = '\\' . $file->getBasename('.' . $file->getExtension());
106 8
            $migration = new $className($this->db);
107
108 8
            $migrations[$migration->getNumber()] = $migration;
109
        }
110
111 8
        ksort($migrations);
112
113 8
        return $migrations;
114
    }
115
116
    /**
117
     * @return AbstractMigration[]
118
     */
119 8
    public function getMigrations()
120
    {
121 8
        if (!isset($this->migrations)) {
122 8
            $this->migrations = $this->loadMigrations();
123
        }
124
125 8
        return $this->migrations;
126
    }
127
128
    /**
129
     * @return int
130
     */
131 8
    public function getLatestNumber()
132
    {
133 8
        $numbers = array_keys($this->getMigrations());
134 8
        $latest = end($numbers);
135
136 8
        return ($latest !== false) ? $latest : 0;
137
    }
138
139
    /**
140
     * @return bool
141
     */
142 8
    private function hasMigrationsTable()
143
    {
144 8
        if (!isset($this->hasMigrationsTable)) {
145 8
            $this->hasMigrationsTable =
146 8
                (bool) $this->db->fetchValue('SHOW TABLES LIKE ?', [$this->migrationsTableName]);
147
        }
148
149 8
        return $this->hasMigrationsTable;
150
    }
151
152
    /**
153
     */
154 8
    protected function createMigrationsTable()
155
    {
156 8
        if (!$this->hasMigrationsTable()) {
157 8
            $this->db->exec('
158 8
                CREATE TABLE `' . $this->migrationsTableName . '` (
159
                    `migration_number` BIGINT NOT NULL,
160
                    `completed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
161
                    PRIMARY KEY (`migration_number`)
162
                )
163
            ');
164 8
            $this->hasMigrationsTable = true;
165
        }
166 8
    }
167
168
    /**
169
     * @return array
170
     */
171 8
    protected function getMigratedNumbers()
172
    {
173 8
        if (!isset($this->migratedNumbers)) {
174 8
            $this->createMigrationsTable();
175
176 8
            $this->migratedNumbers = [];
177
178 8
            $sql = 'SELECT * FROM `' . $this->migrationsTableName . '` ORDER BY migration_number';
179 8
            foreach ($this->db->fetchRows($sql) as $row) {
180 8
                $this->migratedNumbers[] = $row['migration_number'];
181
            }
182
        }
183
184 8
        return $this->migratedNumbers;
185
    }
186
187
    /**
188
     * @return int
189
     */
190 8
    public function getCurrentNumber()
191
    {
192 8
        $migratedNumbers = $this->getMigratedNumbers();
193 8
        $current = end($migratedNumbers);
194
195 8
        return ($current !== false) ? $current : 0;
196
    }
197
198
    /**
199
     * @param int $toNumber
200
     * @return string
201
     */
202 4
    protected function getDirection($toNumber)
203
    {
204 4
        return ($this->getCurrentNumber() > $toNumber) ? self::DIRECTION_DOWN : self::DIRECTION_UP;
205
    }
206
207
    /**
208
     * @param int $to
209
     * @return AbstractMigration[]
210
     */
211 4
    public function getMigrationsTo($to = null)
212
    {
213 4
        $toNumber = $this->getToNumber($to);
214 4
        $allMigrations = $this->getMigrations();
215 4
        $direction = $this->getDirection($toNumber);
216 4
        if ($direction === self::DIRECTION_DOWN) {
217 1
            $allMigrations = array_reverse($allMigrations, true);
218
        }
219
220 4
        $migrations = [];
221 4
        foreach ($allMigrations as $migrationNumber => $migration) {
222 4
            if ($this->shouldMigrationBeMigrated($migration, $toNumber, $direction)) {
223 4
                $migrations[$migrationNumber] = $migration;
224
            }
225
        }
226
227 4
        return $migrations;
228
    }
229
230
    /**
231
     * @param AbstractMigration $migration
232
     * @param int               $toNumber
233
     * @param string            $direction
234
     * @return bool
235
     */
236 4
    private function shouldMigrationBeMigrated(AbstractMigration $migration, $toNumber, $direction)
237
    {
238 4
        if (($direction === self::DIRECTION_UP
239 4
                && $migration->getNumber() <= $toNumber
240 3
                && !in_array($migration->getNumber(), $this->getMigratedNumbers()))
241 4
            || ($direction == self::DIRECTION_DOWN
242 4
                && $migration->getNumber() > $toNumber
243 4
                && in_array($migration->getNumber(), $this->getMigratedNumbers()))
244
        ) {
245 3
            return true;
246
        }
247
248 4
        return false;
249
    }
250
251
    /**
252
     * @param AbstractMigration $migration
253
     */
254 2
    protected function addMigratedMigration(AbstractMigration $migration)
255
    {
256 2
        $this->createMigrationsTable();
257
258 2
        $this->db->exec(
259 2
            'INSERT INTO `' . $this->migrationsTableName . '` SET migration_number = ?',
260 2
            [$migration->getNumber()]
261
        );
262
263 2
        $this->migratedNumbers[] = $migration->getNumber();
264 2
    }
265
266
    /**
267
     * @param AbstractMigration $migration
268
     */
269 1
    protected function deleteMigratedMigration(AbstractMigration $migration)
270
    {
271 1
        $this->createMigrationsTable();
272
273 1
        $this->db->exec(
274 1
            'DELETE FROM `' . $this->migrationsTableName . '` WHERE migration_number = ?',
275 1
            [$migration->getNumber()]
276
        );
277
278 1
        if (($key = array_search($migration->getNumber(), $this->migratedNumbers)) !== false) {
279 1
            unset($this->migratedNumbers[$key]);
280
        }
281 1
    }
282
283
    /**
284
     * Empty database.
285
     */
286 1
    public function emptyDb()
287
    {
288 1
        if (($rows = $this->db->fetchRows('SHOW TABLES', [], true))) {
289 1
            $this->db->exec('SET foreign_key_checks = 0');
290 1
            foreach ($rows as $row) {
291 1
                $this->db->exec('DROP TABLE `' . $row[0] . '`');
292
            }
293 1
            $this->db->exec('SET foreign_key_checks = 1');
294
        }
295 1
    }
296
297
    /**
298
     * @param int|null $to
299
     * @return int
300
     * @throws \InvalidArgumentException
301
     */
302 6
    protected function getToNumber($to = null)
303
    {
304 6
        if ($to === null) {
305 3
            return $this->getLatestNumber();
306 6
        } elseif (!preg_match('/^\d+$/', $to)) {
307 1
            throw new \InvalidArgumentException('Migration number must be a number');
308
        }
309
310 5
        $toNumber = (int) $to;
311
312 5
        if (!in_array($toNumber, array_keys($this->getMigrations()))) {
313 1
             throw new \InvalidArgumentException('Invalid migration number');
314
        }
315
316 4
        return $toNumber;
317
    }
318
319
    /**
320
     * @param int|null $to A migration number to migrate to (if not provided, latest migration number will be used)
321
     * @return bool Returns true if any action was performed
322
     * @throws \InvalidArgumentException
323
     * @throws \RuntimeException
324
     */
325 7
    public function migrate($to = null)
326
    {
327 7
        if ($this->getCurrentNumber() > $this->getLatestNumber()) {
328 1
            throw new \RuntimeException(sprintf(
329 1
                'The current migration number (%d) is higher than latest available (%d). Something is wrong!',
330 1
                $this->getCurrentNumber(),
331 1
                $this->getLatestNumber()
332
            ));
333
        }
334
335 6
        $toNumber = $this->getToNumber($to);
336 4
        $migrations = $this->getMigrationsTo($toNumber);
337
338
        // If there's no migration to be done, we are up to date.
339 4
        if (empty($migrations)) {
340 1
            $this->addInfo(sprintf(
341 1
                'No migrations available, things are up to date (migration %d)!',
342 1
                $this->getCurrentNumber()
343
            ));
344
345 1
            return false;
346
        }
347
348 3
        $this->addInfo(sprintf(
349 3
            'Running %d migrations from migration %d to %d...',
350 3
            count($migrations),
351 3
            $this->getCurrentNumber(),
352 3
            $toNumber
353
        ));
354
355 3
        $this->runMigrations($migrations, $this->getDirection($toNumber));
356
357 3
        $this->addInfo(sprintf('Done! %s migrations migrated!', count($migrations)));
358
359 3
        return true;
360
    }
361
362
    /**
363
     * @param AbstractMigration[] $migrations
364
     * @param string $direction
365
     */
366 3
    protected function runMigrations(array $migrations, $direction)
367
    {
368 3
        foreach ($migrations as $migration) {
369 3
            if ($direction === self::DIRECTION_UP) {
370 2
                $this->addInfo(sprintf(' - Migrating up %d...', $migration->getNumber()));
371 2
                $migration->up();
372 2
                $this->addMigratedMigration($migration);
373
            } else {
374 1
                $this->addInfo(sprintf(' - Migrating down %d...', $migration->getNumber()));
375 1
                $migration->down();
376 3
                $this->deleteMigratedMigration($migration);
377
            }
378
        }
379 3
    }
380
381
    /**
382
     * @param string $info
383
     */
384 4
    protected function addInfo($info)
385
    {
386 4
        if ($this->infoCallback) {
387 4
            $callback = $this->infoCallback;
388 4
            $callback($info);
389
        }
390 4
    }
391
}
392