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.
Completed
Push — master ( f500e6...8bb85a )
by Andreas
02:52
created

Migrator   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 377
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 99.39%

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 2
dl 0
loc 377
ccs 162
cts 163
cp 0.9939
rs 8.3206
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A findMigrationFiles() 0 12 4
A loadMigrations() 0 16 2
A getMigrations() 0 8 2
A getLatestNumber() 0 8 2
A hasMigrationsTable() 0 9 2
A createMigrationsTable() 0 13 2
A getMigratedNumbers() 0 15 3
A getCurrentNumber() 0 7 2
A getDirection() 0 4 2
A getMigrationsTo() 0 18 4
B shouldMigrationBeMigrated() 0 14 7
A addMigratedMigration() 0 11 1
A deleteMigratedMigration() 0 13 2
A emptyDb() 0 10 3
A getToNumber() 0 16 4
B migrate() 0 36 3
A runMigrations() 0 16 3
A addInfo() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like Migrator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Migrator, and based on these observations, apply Extract Interface, too.

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 8
            }
91 8
        }
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 8
        }
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 8
        }
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
    /**
141
     * @return bool
142
     */
143 8
    private function hasMigrationsTable()
144
    {
145 8
        if (!isset($this->hasMigrationsTable)) {
146 8
            $this->hasMigrationsTable =
147 8
                (bool) $this->db->fetchValue('SHOW TABLES LIKE ?', [$this->migrationsTableName]);
148 8
        }
149
150 8
        return $this->hasMigrationsTable;
151
    }
152
153
    /**
154
     */
155 8
    protected function createMigrationsTable()
156
    {
157 8
        if (!$this->hasMigrationsTable()) {
158 8
            $this->db->exec('
159 8
                CREATE TABLE `' . $this->migrationsTableName . '` (
160
                    `migration_number` BIGINT NOT NULL,
161
                    `completed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
162
                    PRIMARY KEY (`migration_number`)
163
                )
164 8
            ');
165 8
            $this->hasMigrationsTable = true;
166 8
        }
167 8
    }
168
169
    /**
170
     * @return array
171
     */
172 8
    protected function getMigratedNumbers()
173
    {
174 8
        if (!isset($this->migratedNumbers)) {
175 8
            $this->createMigrationsTable();
176
177 8
            $this->migratedNumbers = [];
178
179 8
            $sql = 'SELECT * FROM `' . $this->migrationsTableName . '` ORDER BY migration_number';
180 8
            foreach ($this->db->fetchRows($sql) as $row) {
181 8
                $this->migratedNumbers[] = $row['migration_number'];
182 8
            }
183 8
        }
184
185 8
        return $this->migratedNumbers;
186
    }
187
188
    /**
189
     * @return int
190
     */
191 8
    public function getCurrentNumber()
192
    {
193 8
        $migratedNumbers = $this->getMigratedNumbers();
194 8
        $current = end($migratedNumbers);
195
196 8
        return ($current !== false) ? $current : 0;
197
    }
198
199
    /**
200
     * @param int $toNumber
201
     * @return string
202
     */
203 4
    protected function getDirection($toNumber)
204
    {
205 4
        return ($this->getCurrentNumber() > $toNumber) ? self::DIRECTION_DOWN : self::DIRECTION_UP;
206
    }
207
208
    /**
209
     * @param int $to
210
     * @return AbstractMigration[]
211
     */
212 4
    public function getMigrationsTo($to = null)
213
    {
214 4
        $toNumber = $this->getToNumber($to);
215 4
        $allMigrations = $this->getMigrations();
216 4
        $direction = $this->getDirection($toNumber);
217 4
        if ($direction === self::DIRECTION_DOWN) {
218 1
            $allMigrations = array_reverse($allMigrations, true);
219 1
        }
220
221 4
        $migrations = [];
222 4
        foreach ($allMigrations as $migrationNumber => $migration) {
223 4
            if ($this->shouldMigrationBeMigrated($migration, $toNumber, $direction)) {
224 3
                $migrations[$migrationNumber] = $migration;
225 3
            }
226 4
        }
227
228 4
        return $migrations;
229
    }
230
231
    /**
232
     * @param AbstractMigration $migration
233
     * @param int               $toNumber
234
     * @param string            $direction
235
     * @return bool
236
     */
237 4
    private function shouldMigrationBeMigrated(AbstractMigration $migration, $toNumber, $direction)
238
    {
239
        if (($direction === self::DIRECTION_UP
240 4
                && $migration->getNumber() <= $toNumber
241 4
                && !in_array($migration->getNumber(), $this->getMigratedNumbers()))
242
            || ($direction == self::DIRECTION_DOWN
243 4
                && $migration->getNumber() > $toNumber
244 4
                && in_array($migration->getNumber(), $this->getMigratedNumbers()))
245 4
        ) {
246 3
            return true;
247
        }
248
249 4
        return false;
250
    }
251
252
    /**
253
     * @param AbstractMigration $migration
254
     */
255 2
    protected function addMigratedMigration(AbstractMigration $migration)
256
    {
257 2
        $this->createMigrationsTable();
258
259 2
        $this->db->exec(
260 2
            'INSERT INTO `' . $this->migrationsTableName . '` SET migration_number = ?',
261 2
            [$migration->getNumber()]
262 2
        );
263
264 2
        $this->migratedNumbers[] = $migration->getNumber();
265 2
    }
266
267
    /**
268
     * @param AbstractMigration $migration
269
     */
270 1
    protected function deleteMigratedMigration(AbstractMigration $migration)
271
    {
272 1
        $this->createMigrationsTable();
273
274 1
        $this->db->exec(
275 1
            'DELETE FROM `' . $this->migrationsTableName . '` WHERE migration_number = ?',
276 1
            [$migration->getNumber()]
277 1
        );
278
279 1
        if (($key = array_search($migration->getNumber(), $this->migratedNumbers)) !== false) {
280 1
            unset($this->migratedNumbers[$key]);
281 1
        }
282 1
    }
283
284
    /**
285
     * Empty database.
286
     */
287 1
    public function emptyDb()
288
    {
289 1
        if (($rows = $this->db->fetchRows('SHOW TABLES', [], true))) {
290 1
            $this->db->exec('SET foreign_key_checks = 0');
291 1
            foreach ($rows as $row) {
292 1
                $this->db->exec('DROP TABLE `' . $row[0] . '`');
293 1
            }
294 1
            $this->db->exec('SET foreign_key_checks = 1');
295 1
        }
296 1
    }
297
298
    /**
299
     * @param int|null $to
300
     * @return int
301
     * @throws \InvalidArgumentException
302
     */
303 6
    protected function getToNumber($to = null)
304
    {
305 6
        if ($to === null) {
306 3
            return $this->getLatestNumber();
307 6
        } elseif (!preg_match('/^\d+$/', $to)) {
308 1
            throw new \InvalidArgumentException('Migration number must be a number');
309
        }
310
311 5
        $toNumber = (int) $to;
312
313 5
        if (!in_array($toNumber, array_keys($this->getMigrations()))) {
314 1
             throw new \InvalidArgumentException('Invalid migration number');
315
        }
316
317 4
        return $toNumber;
318
    }
319
320
    /**
321
     * @param int|null $to A migration number to migrate to (if not provided, latest migration number will be used)
322
     * @return bool Returns true if any action was performed
323
     * @throws \InvalidArgumentException
324
     * @throws \RuntimeException
325
     */
326 7
    public function migrate($to = null)
327
    {
328 7
        if ($this->getCurrentNumber() > $this->getLatestNumber()) {
329 1
            throw new \RuntimeException(sprintf(
330 1
                'The current migration number (%d) is higher than latest available (%d). Something is wrong!',
331 1
                $this->getCurrentNumber(),
332 1
                $this->getLatestNumber()
333 1
            ));
334
        }
335
336 6
        $toNumber = $this->getToNumber($to);
337 4
        $migrations = $this->getMigrationsTo($toNumber);
338
339
        // If there's no migration to be done, we are up to date.
340 4
        if (empty($migrations)) {
341 1
            $this->addInfo(sprintf(
342 1
                'No migrations available, things are up to date (migration %d)!',
343 1
                $this->getCurrentNumber()
344 1
            ));
345
346 1
            return false;
347
        }
348
349 3
        $this->addInfo(sprintf(
350 3
            'Running %d migrations from migration %d to %d...',
351 3
            count($migrations),
352 3
            $this->getCurrentNumber(),
353
            $toNumber
354 3
        ));
355
356 3
        $this->runMigrations($migrations, $this->getDirection($toNumber));
357
358 3
        $this->addInfo(sprintf('Done! %s migrations migrated!', count($migrations)));
359
360 3
        return true;
361
    }
362
363
    /**
364
     * @param AbstractMigration[] $migrations
365
     * @param string $direction
366
     */
367 3
    protected function runMigrations(array $migrations, $direction)
368
    {
369 3
        foreach ($migrations as $migration) {
370 3
            if ($direction === self::DIRECTION_UP) {
371 2
                $this->addInfo(sprintf(' - Migrating up %d...', $migration->getNumber()));
372 2
                $migration->up();
373 2
                $this->addMigratedMigration($migration);
374
375 2
            } else {
376 1
                $this->addInfo(sprintf(' - Migrating down %d...', $migration->getNumber()));
377 1
                $migration->down();
378 1
                $this->deleteMigratedMigration($migration);
379
380
            }
381 3
        }
382 3
    }
383
384
    /**
385
     * @param string $info
386
     */
387 4
    protected function addInfo($info)
388
    {
389 4
        if ($this->infoCallback) {
390 4
            $callback = $this->infoCallback;
391 4
            $callback($info);
392 4
        }
393 4
    }
394
}
395