Failed Conditions
Pull Request — master (#632)
by Michael
02:44
created

Configuration::resolveVersionAlias()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 8.0155

Importance

Changes 0
Metric Value
cc 8
eloc 17
nc 8
nop 1
dl 0
loc 28
ccs 15
cts 16
cp 0.9375
crap 8.0155
rs 5.3846
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\Migrations\Configuration;
6
7
use DateTime;
8
use DateTimeInterface;
9
use DateTimeZone;
10
use Doctrine\Common\EventArgs;
11
use Doctrine\DBAL\Connection;
12
use Doctrine\DBAL\Connections\MasterSlaveConnection;
13
use Doctrine\DBAL\Schema\Column;
14
use Doctrine\DBAL\Schema\Table;
15
use Doctrine\DBAL\Types\Type;
16
use Doctrine\Migrations\FileQueryWriter;
17
use Doctrine\Migrations\Finder\MigrationDeepFinder;
18
use Doctrine\Migrations\Finder\MigrationFinder;
19
use Doctrine\Migrations\Finder\RecursiveRegexFinder;
20
use Doctrine\Migrations\MigrationException;
21
use Doctrine\Migrations\OutputWriter;
22
use Doctrine\Migrations\QueryWriter;
23
use Doctrine\Migrations\Version;
24
use const SORT_STRING;
25
use function array_combine;
26
use function array_keys;
27
use function array_map;
28
use function array_reverse;
29
use function array_search;
30
use function array_unshift;
31
use function array_values;
32
use function class_exists;
33
use function count;
34
use function end;
35
use function get_class;
36
use function implode;
37
use function in_array;
38
use function ksort;
39
use function sprintf;
40
use function str_replace;
41
use function substr;
42
43
class Configuration
44
{
45
    public const VERSIONS_ORGANIZATION_BY_YEAR = 'year';
46
47
    public const VERSIONS_ORGANIZATION_BY_YEAR_AND_MONTH = 'year_and_month';
48
49
    public const VERSION_FORMAT = 'YmdHis';
50
51
    /** @var string */
52
    private $name;
53
54
    /** @var bool */
55
    private $migrationTableCreated = false;
56
57
    /** @var Connection */
58
    private $connection;
59
60
    /** @var OutputWriter */
61
    private $outputWriter;
62
63
    /** @var MigrationFinder */
64
    private $migrationFinder;
65
66
    /** @var null|QueryWriter */
67
    private $queryWriter;
68
69
    /** @var string */
70
    private $migrationsTableName = 'doctrine_migration_versions';
71
72
    /** @var string */
73
    private $migrationsColumnName = 'version';
74
75
    /** @var string */
76
    private $migrationsDirectory;
77
78
    /** @var string */
79
    private $migrationsNamespace;
80
81
    /** @var Version[] */
82
    private $migrations = [];
83
84
    /** @var bool */
85
    private $migrationsAreOrganizedByYear = false;
86
87
    /** @var bool */
88
    private $migrationsAreOrganizedByYearAndMonth = false;
89
90
    /** @var null|string */
91
    private $customTemplate;
92
93
    /** @var bool */
94
    private $isDryRun = false;
95
96 260
    public function __construct(
97
        Connection $connection,
98
        ?OutputWriter $outputWriter = null,
99
        ?MigrationFinder $finder = null,
100
        ?QueryWriter $queryWriter = null
101
    ) {
102 260
        $this->connection      = $connection;
103 260
        $this->outputWriter    = $outputWriter ?? new OutputWriter();
104 260
        $this->migrationFinder = $finder ?? new RecursiveRegexFinder();
105 260
        $this->queryWriter     = $queryWriter;
106 260
    }
107
108 22
    public function areMigrationsOrganizedByYear() : bool
109
    {
110 22
        return $this->migrationsAreOrganizedByYear;
111
    }
112
113 22
    public function areMigrationsOrganizedByYearAndMonth() : bool
114
    {
115 22
        return $this->migrationsAreOrganizedByYearAndMonth;
116
    }
117
118
    /** @throws MigrationException */
119 130
    public function validate() : void
120
    {
121 130
        if (! $this->migrationsNamespace) {
122 1
            throw MigrationException::migrationsNamespaceRequired();
123
        }
124
125 129
        if (! $this->migrationsDirectory) {
126 1
            throw MigrationException::migrationsDirectoryRequired();
127
        }
128 128
    }
129
130 63
    public function setName(string $name) : void
131
    {
132 63
        $this->name = $name;
133 63
    }
134
135 16
    public function getName() : ?string
136
    {
137 16
        return $this->name;
138
    }
139
140 5
    public function setOutputWriter(OutputWriter $outputWriter) : void
141
    {
142 5
        $this->outputWriter = $outputWriter;
143 5
    }
144
145 125
    public function getOutputWriter() : OutputWriter
146
    {
147 125
        return $this->outputWriter;
148
    }
149
150 15
    public function getDateTime(string $version) : string
151
    {
152 15
        $datetime = str_replace('Version', '', $version);
153 15
        $datetime = DateTime::createFromFormat('YmdHis', $datetime);
154
155 15
        if ($datetime === false) {
156 5
            return '';
157
        }
158
159 11
        return $datetime->format('Y-m-d H:i:s');
160
    }
161
162 128
    public function getConnection() : Connection
163
    {
164 128
        return $this->connection;
165
    }
166
167 85
    public function setMigrationsTableName(string $tableName) : void
168
    {
169 85
        $this->migrationsTableName = $tableName;
170 85
    }
171
172 62
    public function getMigrationsTableName() : string
173
    {
174 62
        return $this->migrationsTableName;
175
    }
176
177 56
    public function setMigrationsColumnName(string $columnName) : void
178
    {
179 56
        $this->migrationsColumnName = $columnName;
180 56
    }
181
182 15
    public function getMigrationsColumnName() : string
183
    {
184 15
        return $this->migrationsColumnName;
185
    }
186
187 70
    public function getQuotedMigrationsColumnName() : string
188
    {
189 70
        return $this->getMigrationsColumn()->getQuotedName($this->connection->getDatabasePlatform());
190
    }
191
192 179
    public function setMigrationsDirectory(string $migrationsDirectory) : void
193
    {
194 179
        $this->migrationsDirectory = $migrationsDirectory;
195 179
    }
196
197 19
    public function getMigrationsDirectory() : ?string
198
    {
199 19
        return $this->migrationsDirectory;
200
    }
201
202 182
    public function setMigrationsNamespace(string $migrationsNamespace) : void
203
    {
204 182
        $this->migrationsNamespace = $migrationsNamespace;
205 182
    }
206
207 93
    public function getMigrationsNamespace() : ?string
208
    {
209 93
        return $this->migrationsNamespace;
210
    }
211
212 5
    public function setCustomTemplate(?string $customTemplate) : void
213
    {
214 5
        $this->customTemplate = $customTemplate;
215 5
    }
216
217 9
    public function getCustomTemplate() : ?string
218
    {
219 9
        return $this->customTemplate;
220
    }
221
222
    /** @throws MigrationException */
223 8
    public function setMigrationsFinder(MigrationFinder $finder) : void
224
    {
225 8
        if (($this->migrationsAreOrganizedByYear || $this->migrationsAreOrganizedByYearAndMonth)
226 8
            && ! ($finder instanceof MigrationDeepFinder)) {
227 4
            throw MigrationException::configurationIncompatibleWithFinder(
228 4
                'organize-migrations',
229 4
                $finder
230
            );
231
        }
232
233 4
        $this->migrationFinder = $finder;
234 4
    }
235
236
    /** @return Version[] */
237 88
    public function registerMigrationsFromDirectory(string $path) : array
238
    {
239 88
        $this->validate();
240
241 88
        return $this->registerMigrations($this->findMigrations($path));
242
    }
243
244
    /** @throws MigrationException */
245 71
    public function registerMigration(string $version, string $class) : Version
246
    {
247 71
        $this->ensureMigrationClassExists($class);
248
249 70
        if (isset($this->migrations[$version])) {
250 1
            throw MigrationException::duplicateMigrationVersion(
251 1
                $version,
252 1
                get_class($this->migrations[$version])
253
            );
254
        }
255
256 70
        $version = new Version($this, $version, $class);
257
258 70
        $this->migrations[$version->getVersion()] = $version;
259
260 70
        ksort($this->migrations, SORT_STRING);
261
262 70
        return $version;
263
    }
264
265
    /**
266
     * @param string[] $migrations
267
     *
268
     * @return Version[]
269
     */
270 92
    public function registerMigrations(array $migrations) : array
271
    {
272 92
        $versions = [];
273
274 92
        foreach ($migrations as $version => $class) {
275 35
            $versions[] = $this->registerMigration((string) $version, $class);
276
        }
277
278 91
        return $versions;
279
    }
280
281
    /**
282
     * @return Version[]
283
     */
284 35
    public function getMigrations() : array
285
    {
286 35
        return $this->migrations;
287
    }
288
289 21
    public function getVersion(string $version) : Version
290
    {
291 21
        $this->loadMigrationsFromDirectory();
292
293 21
        if (! isset($this->migrations[$version])) {
294 2
            throw MigrationException::unknownMigrationVersion($version);
295
        }
296
297 19
        return $this->migrations[$version];
298
    }
299
300 20
    public function hasVersion(string $version) : bool
301
    {
302 20
        $this->loadMigrationsFromDirectory();
303
304 20
        return isset($this->migrations[$version]);
305
    }
306
307 21
    public function hasVersionMigrated(Version $version) : bool
308
    {
309 21
        $this->connect();
310 21
        $this->createMigrationTable();
311
312 21
        $version = $this->connection->fetchColumn(
313 21
            'SELECT ' . $this->getQuotedMigrationsColumnName() . ' FROM ' . $this->migrationsTableName . ' WHERE ' . $this->getQuotedMigrationsColumnName() . ' = ?',
314 21
            [$version->getVersion()]
315
        );
316
317 21
        return $version !== false;
318
    }
319
320
    /** @return string[] */
321 42
    public function getMigratedVersions() : array
322
    {
323 42
        $this->createMigrationTable();
324
325 42
        if (! $this->migrationTableCreated && $this->isDryRun) {
326 1
            return [];
327
        }
328
329 42
        $this->connect();
330
331 42
        $sql = sprintf(
332 42
            'SELECT %s FROM %s',
333 42
            $this->getQuotedMigrationsColumnName(),
334 42
            $this->migrationsTableName
335
        );
336
337 42
        $result = $this->connection->fetchAll($sql);
338
339 42
        return array_map('current', $result);
340
    }
341
342
    /** @return string[] */
343 14
    public function getAvailableVersions() : array
344
    {
345 14
        $availableVersions = [];
346
347 14
        $this->loadMigrationsFromDirectory();
348
349 14
        foreach ($this->migrations as $migration) {
350 14
            $availableVersions[] = $migration->getVersion();
351
        }
352
353 14
        return $availableVersions;
354
    }
355
356 42
    public function getCurrentVersion() : string
357
    {
358 42
        $this->createMigrationTable();
359
360 42
        if (! $this->migrationTableCreated && $this->isDryRun) {
361 1
            return '0';
362
        }
363
364 42
        $this->connect();
365
366 42
        $this->loadMigrationsFromDirectory();
367
368 42
        $where = null;
369
370 42
        if (! empty($this->migrations)) {
371 38
            $migratedVersions = [];
372
373 38
            foreach ($this->migrations as $migration) {
374 38
                $migratedVersions[] = sprintf("'%s'", $migration->getVersion());
375
            }
376
377 38
            $where = sprintf(
378 38
                ' WHERE %s IN (%s)',
379 38
                $this->getQuotedMigrationsColumnName(),
380 38
                implode(', ', $migratedVersions)
381
            );
382
        }
383
384 42
        $sql = sprintf(
385 42
            'SELECT %s FROM %s%s ORDER BY %s DESC',
386 42
            $this->getQuotedMigrationsColumnName(),
387 42
            $this->migrationsTableName,
388 42
            $where,
389 42
            $this->getQuotedMigrationsColumnName()
390
        );
391
392 42
        $sql    = $this->connection->getDatabasePlatform()->modifyLimitQuery($sql, 1);
393 42
        $result = $this->connection->fetchColumn($sql);
394
395 42
        return $result !== false ? (string) $result : '0';
396
    }
397
398 10
    public function getPrevVersion() : ?string
399
    {
400 10
        return $this->getRelativeVersion($this->getCurrentVersion(), -1);
401
    }
402
403 11
    public function getNextVersion() : ?string
404
    {
405 11
        return $this->getRelativeVersion($this->getCurrentVersion(), 1);
406
    }
407
408 15
    public function getRelativeVersion(string $version, int $delta) : ?string
409
    {
410 15
        $this->loadMigrationsFromDirectory();
411
412 15
        $versions = array_map('strval', array_keys($this->migrations));
413
414 15
        array_unshift($versions, '0');
415
416 15
        $offset = array_search($version, $versions, true);
417
418 15
        if ($offset === false || ! isset($versions[$offset + $delta])) {
419
            // Unknown version or delta out of bounds.
420 11
            return null;
421
        }
422
423 13
        return $versions[$offset + $delta];
424
    }
425
426 1
    public function getDeltaVersion(string $delta) : ?string
427
    {
428 1
        $symbol = substr($delta, 0, 1);
429 1
        $number = (int) substr($delta, 1);
430
431 1
        if ($number <= 0) {
432
            return null;
433
        }
434
435 1
        if ($symbol === '+' || $symbol === '-') {
436 1
            return $this->getRelativeVersion($this->getCurrentVersion(), (int) $delta);
437
        }
438
439
        return null;
440
    }
441
442
    /**
443
     * Returns the version number from an alias.
444
     *
445
     * Supported aliases are:
446
     *
447
     * - first: The very first version before any migrations have been run.
448
     * - current: The current version.
449
     * - prev: The version prior to the current version.
450
     * - next: The version following the current version.
451
     * - latest: The latest available version.
452
     *
453
     * If an existing version number is specified, it is returned verbatimly.
454
     */
455 9
    public function resolveVersionAlias(string $alias) : ?string
456
    {
457 9
        if ($this->hasVersion($alias)) {
458 1
            return $alias;
459
        }
460
461
        switch ($alias) {
462 9
            case 'first':
463 1
                return '0';
464
465 9
            case 'current':
466 9
                return $this->getCurrentVersion();
467
468 9
            case 'prev':
469 9
                return $this->getPrevVersion();
470
471 9
            case 'next':
472 9
                return $this->getNextVersion();
473
474 9
            case 'latest':
475 9
                return $this->getLatestVersion();
476
477
            default:
478 1
                if (substr($alias, 0, 7) === 'current') {
479
                    return $this->getDeltaVersion(substr($alias, 7));
480
                }
481
482 1
                return null;
483
        }
484
    }
485
486 1
    public function getNumberOfExecutedMigrations() : int
487
    {
488 1
        $this->connect();
489 1
        $this->createMigrationTable();
490
491 1
        $sql = sprintf(
492 1
            'SELECT COUNT(%s) FROM %s',
493 1
            $this->getQuotedMigrationsColumnName(),
494 1
            $this->migrationsTableName
495
        );
496
497 1
        $result = $this->connection->fetchColumn($sql);
498
499 1
        return $result !== false ? (int) $result : 0;
500
    }
501
502 3
    public function getNumberOfAvailableMigrations() : int
503
    {
504 3
        $this->loadMigrationsFromDirectory();
505
506 3
        return count($this->migrations);
507
    }
508
509 26
    public function getLatestVersion() : string
510
    {
511 26
        $this->loadMigrationsFromDirectory();
512
513 26
        $versions = array_keys($this->migrations);
514 26
        $latest   = end($versions);
515
516 26
        return $latest !== false ? (string) $latest : '0';
517
    }
518
519 64
    public function createMigrationTable() : bool
520
    {
521 64
        $this->validate();
522
523 64
        if ($this->migrationTableCreated) {
524 52
            return false;
525
        }
526
527 64
        $this->connect();
528
529 64
        if ($this->connection->getSchemaManager()->tablesExist([$this->migrationsTableName])) {
530 4
            $this->migrationTableCreated = true;
531
532 4
            return false;
533
        }
534
535 63
        if ($this->isDryRun) {
536 1
            return false;
537
        }
538
539
        $columns = [
540 63
            $this->migrationsColumnName => $this->getMigrationsColumn(),
541
        ];
542
543 63
        $table = new Table($this->migrationsTableName, $columns);
544 63
        $table->setPrimaryKey([$this->migrationsColumnName]);
545
546 63
        $this->connection->getSchemaManager()->createTable($table);
547
548 63
        $this->migrationTableCreated = true;
549
550 63
        return true;
551
    }
552
553
    /** @return Version[] */
554 33
    public function getMigrationsToExecute(string $direction, string $to) : array
555
    {
556 33
        $this->loadMigrationsFromDirectory();
557
558 33
        if ($direction === Version::DIRECTION_DOWN) {
559 7
            if (count($this->migrations)) {
560 7
                $allVersions = array_reverse(array_keys($this->migrations));
561 7
                $classes     = array_reverse(array_values($this->migrations));
562 7
                $allVersions = array_combine($allVersions, $classes);
563
            } else {
564 7
                $allVersions = [];
565
            }
566
        } else {
567 31
            $allVersions = $this->migrations;
568
        }
569
570 33
        $versions = [];
571 33
        $migrated = $this->getMigratedVersions();
572
573 33
        foreach ($allVersions as $version) {
574 31
            if (! $this->shouldExecuteMigration($direction, $version, $to, $migrated)) {
575 18
                continue;
576
            }
577
578 28
            $versions[$version->getVersion()] = $version;
579
        }
580
581 33
        return $versions;
582
    }
583
584 51
    public function dispatchEvent(string $eventName, ?EventArgs $args = null) : void
585
    {
586 51
        $this->connection->getEventManager()->dispatchEvent($eventName, $args);
587 51
    }
588
589
    /**
590
     * @return string[]
591
     */
592 88
    protected function findMigrations(string $path) : array
593
    {
594 88
        return $this->migrationFinder->findMigrations($path, $this->getMigrationsNamespace());
595
    }
596
597
    /**
598
     * @throws MigrationException
599
     */
600 9
    public function setMigrationsAreOrganizedByYear(bool $migrationsAreOrganizedByYear = true) : void
601
    {
602 9
        $this->ensureOrganizeMigrationsIsCompatibleWithFinder();
603
604 5
        $this->migrationsAreOrganizedByYear = $migrationsAreOrganizedByYear;
605 5
    }
606
607
    /**
608
     * @throws MigrationException
609
     */
610 10
    public function setMigrationsAreOrganizedByYearAndMonth(bool $migrationsAreOrganizedByYearAndMonth = true) : void
611
    {
612 10
        $this->ensureOrganizeMigrationsIsCompatibleWithFinder();
613
614 10
        $this->migrationsAreOrganizedByYear         = $migrationsAreOrganizedByYearAndMonth;
615 10
        $this->migrationsAreOrganizedByYearAndMonth = $migrationsAreOrganizedByYearAndMonth;
616 10
    }
617
618 9
    public function generateVersionNumber(?DateTimeInterface $now = null) : string
619
    {
620 9
        $now = $now ?: new DateTime('now', new DateTimeZone('UTC'));
621
622 9
        return $now->format(self::VERSION_FORMAT);
623
    }
624
625
    /**
626
     * Explicitely opens the database connection. This is done to play nice
627
     * with DBAL's MasterSlaveConnection. Which, in some cases, connects to a
628
     * follower when fetching the executed migrations. If a follower is lagging
629
     * significantly behind that means the migrations system may see unexecuted
630
     * migrations that were actually executed earlier.
631
     */
632 64
    protected function connect() : bool
633
    {
634 64
        if ($this->connection instanceof MasterSlaveConnection) {
635 1
            return $this->connection->connect('master');
636
        }
637
638 63
        return $this->connection->connect();
639
    }
640
641
    /**
642
     * @throws MigrationException
643
     */
644 19
    private function ensureOrganizeMigrationsIsCompatibleWithFinder() : void
645
    {
646 19
        if (! ($this->migrationFinder instanceof MigrationDeepFinder)) {
647 4
            throw MigrationException::configurationIncompatibleWithFinder(
648 4
                'organize-migrations',
649 4
                $this->migrationFinder
650
            );
651
        }
652 15
    }
653
654
    /** @param string[] $migrated */
655 31
    private function shouldExecuteMigration(
656
        string $direction,
657
        Version $version,
658
        string $to,
659
        array $migrated
660
    ) : bool {
661 31
        $to = (int) $to;
662
663 31
        if ($direction === Version::DIRECTION_DOWN) {
664 7
            if (! in_array($version->getVersion(), $migrated, true)) {
665 4
                return false;
666
            }
667
668 5
            return $version->getVersion() > $to;
669
        }
670
671 29
        if ($direction === Version::DIRECTION_UP) {
672 29
            if (in_array($version->getVersion(), $migrated, true)) {
673 8
                return false;
674
            }
675
676 28
            return $version->getVersion() <= $to;
677
        }
678
679
        return false;
680
    }
681
682
    /** @throws MigrationException */
683 71
    private function ensureMigrationClassExists(string $class) : void
684
    {
685 71
        if (! class_exists($class)) {
686 1
            throw MigrationException::migrationClassNotFound(
687 1
                $class,
688 1
                $this->getMigrationsNamespace()
689
            );
690
        }
691 70
    }
692
693 8
    public function getQueryWriter() : QueryWriter
694
    {
695 8
        if ($this->queryWriter === null) {
696 7
            $this->queryWriter = new FileQueryWriter(
697 7
                $this->getQuotedMigrationsColumnName(),
698 7
                $this->migrationsTableName,
699 7
                $this->outputWriter
700
            );
701
        }
702
703 8
        return $this->queryWriter;
704
    }
705
706 2
    public function setIsDryRun(bool $isDryRun) : void
707
    {
708 2
        $this->isDryRun = $isDryRun;
709 2
    }
710
711 70
    private function loadMigrationsFromDirectory() : void
712
    {
713 70
        if (! empty($this->migrations) || ! $this->migrationsDirectory) {
714 49
            return;
715
        }
716
717 32
        $this->registerMigrationsFromDirectory($this->migrationsDirectory);
718 32
    }
719
720 70
    private function getMigrationsColumn() : Column
721
    {
722 70
        return new Column(
723 70
            $this->migrationsColumnName,
724 70
            Type::getType('string'),
725 70
            ['length' => 255]
726
        );
727
    }
728
}
729