Passed
Pull Request — master (#41)
by
unknown
03:10
created

Configuration::getCurrentVersion()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 4
nop 0
dl 0
loc 24
ccs 0
cts 14
cp 0
crap 20
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the AntiMattr MongoDB Migrations Library, a library by Matthew Fitzgerald.
5
 *
6
 * (c) 2014 Matthew Fitzgerald
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace AntiMattr\MongoDB\Migrations\Configuration;
13
14
use AntiMattr\MongoDB\Migrations\Exception\ConfigurationValidationException;
15
use AntiMattr\MongoDB\Migrations\Exception\DuplicateVersionException;
16
use AntiMattr\MongoDB\Migrations\Exception\UnknownVersionException;
17
use AntiMattr\MongoDB\Migrations\OutputWriter;
18
use AntiMattr\MongoDB\Migrations\Version;
19
use MongoDB\Client;
20
21
/**
22
 * @author Matthew Fitzgerald <[email protected]>
23
 */
24
class Configuration
25
{
26
    /**
27
     * @var \MongoDB\Collection
28
     */
29
    private $collection;
30
31
    /**
32
     * @var \MongoDB\Client
33
     */
34
    private $connection;
35
36
    /**
37
     * @var \MongoDB\Database
38
     */
39
    private $database;
40
41
    /**
42
     * The migration database name to track versions in.
43
     *
44
     * @var string
45
     */
46
    private $migrationsDatabaseName;
47
48
    /**
49
     * Flag for whether or not the migration collection has been created.
50
     *
51
     * @var bool
52
     */
53
    private $migrationCollectionCreated = false;
54
55
    /**
56
     * The migration collection name to track versions in.
57
     *
58
     * @var string
59
     */
60
    private $migrationsCollectionName = 'antimattr_migration_versions';
61
62
    /**
63
     * The path to a directory where new migration classes will be written.
64
     *
65
     * @var string
66
     */
67
    private $migrationsDirectory;
68
69
    /**
70
     * Namespace the migration classes live in.
71
     *
72
     * @var string
73
     */
74
    private $migrationsNamespace;
75
76
    /**
77
     * The path to a directory where mongo console scripts are.
78
     *
79
     * @var string
80
     */
81
    private $migrationsScriptDirectory;
82
83
    /**
84
     * Used by Console Commands and Output Writer.
85
     *
86
     * @var string
87
     */
88
    private $name;
89
90
    /**
91
     * @var \AntiMattr\MongoDB\Migrations\Version[]
92
     */
93
    protected $migrations = [];
94
95
    /**
96
     * @var \AntiMattr\MongoDB\Migrations\OutputWriter
97
     */
98
    private $outputWriter;
99
100
    /**
101
     * @var string
102
     */
103
    private $file;
104
105
    /**
106
     * @param \MongoDB\Client               $connection
107
     * @param \AntiMattr\MongoDB\Migrations\OutputWriter $outputWriter
108
     */
109 14
    public function __construct(Client $connection, OutputWriter $outputWriter = null)
110
    {
111 14
        $this->connection = $connection;
112 14
        if (null === $outputWriter) {
113 11
            $outputWriter = new OutputWriter();
114
        }
115 14
        $this->outputWriter = $outputWriter;
116 14
    }
117
118
    /**
119
     * Returns a timestamp version as a formatted date.
120
     *
121
     * @param string $version
122
     *
123
     * @return string The formatted version
124
     */
125 5
    public static function formatVersion($version)
126
    {
127 5
        return sprintf('%s-%s-%s %s:%s:%s',
128 5
            substr($version, 0, 4),
129 5
            substr($version, 4, 2),
130 5
            substr($version, 6, 2),
131 5
            substr($version, 8, 2),
132 5
            substr($version, 10, 2),
133 5
            substr($version, 12, 2)
134
        );
135
    }
136
137
    /**
138
     * Returns an array of available migration version numbers.
139
     *
140
     * @return array
141
     */
142 2
    public function getAvailableVersions()
143
    {
144 2
        $availableVersions = [];
145 2
        foreach ($this->migrations as $migration) {
146 1
            $availableVersions[] = $migration->getVersion();
147
        }
148
149 2
        return $availableVersions;
150
    }
151
152
    /**
153
     * @return \MongoDB\Collection
154
     */
155 3
    public function getCollection()
156
    {
157 3
        if (isset($this->collection)) {
158 2
            return $this->collection;
159
        }
160
161 3
        $this->collection = $this->getDatabase()->selectCollection($this->migrationsCollectionName);
162
163 3
        return $this->collection;
164
    }
165
166
    /**
167
     * @return \MongoDB\Client
168
     */
169 5
    public function getConnection()
170
    {
171 5
        return $this->connection;
172
    }
173
174
    /**
175
     * @return \MongoDB\Database
176
     */
177 5
    public function getDatabase(): ?\MongoDB\Database
178
    {
179 5
        if (isset($this->database)) {
180
            return $this->database;
181
        }
182
183 5
        $this->database = $this->connection->selectDatabase($this->migrationsDatabaseName);
184
185 5
        return $this->database;
186
    }
187
188
    /**
189
     * Get the array of registered migration versions.
190
     *
191
     * @return Version[] $migrations
192
     */
193 2
    public function getMigrations()
194
    {
195 2
        return $this->migrations;
196
    }
197
198
    /**
199
     * @param string $databaseName
200
     */
201 9
    public function setMigrationsDatabaseName($databaseName)
202
    {
203 9
        $this->migrationsDatabaseName = $databaseName;
204
205 9
        return $this;
206
    }
207
208
    /**
209
     * @return string
210
     */
211 2
    public function getMigrationsDatabaseName()
212
    {
213 2
        return $this->migrationsDatabaseName;
214
    }
215
216
    /**
217
     * @param string $collectionName
218
     */
219 8
    public function setMigrationsCollectionName($collectionName)
220
    {
221 8
        $this->migrationsCollectionName = $collectionName;
222
223 8
        return $this;
224
    }
225
226
    /**
227
     * @return string
228
     */
229 2
    public function getMigrationsCollectionName()
230
    {
231 2
        return $this->migrationsCollectionName;
232
    }
233
234
    /**
235
     * @param string $migrationsDirectory
236
     */
237 6
    public function setMigrationsDirectory($migrationsDirectory)
238
    {
239 6
        $this->migrationsDirectory = $migrationsDirectory;
240
241 6
        return $this;
242
    }
243
244
    /**
245
     * @return string
246
     */
247 2
    public function getMigrationsDirectory()
248
    {
249 2
        return $this->migrationsDirectory;
250
    }
251
252
    /**
253
     * Set the migrations namespace.
254
     *
255
     * @param string $migrationsNamespace The migrations namespace
256
     */
257 8
    public function setMigrationsNamespace($migrationsNamespace)
258
    {
259 8
        $this->migrationsNamespace = $migrationsNamespace;
260
261 8
        return $this;
262
    }
263
264
    /**
265
     * @return string $migrationsNamespace
266
     */
267 2
    public function getMigrationsNamespace()
268
    {
269 2
        return $this->migrationsNamespace;
270
    }
271
272
    /**
273
     * @param string $scriptsDirectory
274
     */
275 2
    public function setMigrationsScriptDirectory($scriptsDirectory)
276
    {
277 2
        $this->migrationsScriptDirectory = $scriptsDirectory;
278
279 2
        return $this;
280
    }
281
282
    /**
283
     * @return string
284
     */
285 2
    public function getMigrationsScriptDirectory()
286
    {
287 2
        return $this->migrationsScriptDirectory;
288
    }
289
290
    /**
291
     * @param string $file
292
     */
293 3
    public function setFile($file)
294
    {
295 3
        $this->file = $file;
296
297 3
        return $this;
298
    }
299
300
    /**
301
     * @return string|null
302
     */
303
    public function getFile(): ?string
304
    {
305
        return $this->file;
306
    }
307
308
    /**
309
     * Returns all migrated versions from the versions collection, in an array.
310
     *
311
     * @return \AntiMattr\MongoDB\Migrations\Version[]
312
     */
313 1
    public function getMigratedVersions()
314
    {
315 1
        $this->createMigrationCollection();
316
317 1
        $cursor = $this->getCollection()->find();
318 1
        $versions = [];
319 1
        foreach ($cursor as $record) {
320 1
            $versions[] = $record['v'];
321
        }
322
323 1
        return $versions;
324
    }
325
326
    /**
327
     * Returns the time a migration occurred.
328
     *
329
     * @param string $version
330
     *
331
     * @return int
332
     *
333
     * @throws AntiMattr\MongoDB\Migrations\Exception\UnknownVersionException Throws exception if migration version does not exist
334
     * @throws DomainException                                                If more than one version exists
335
     */
336
    public function getMigratedTimestamp($version): int
337
    {
338
        $this->createMigrationCollection();
339
340
        $cursor = $this->getCollection()->find(
341
            ['v' => $version]
342
        );
343
344
        if (!$cursor->count()) {
0 ignored issues
show
Bug introduced by
The method count() does not exist on MongoDB\Driver\Cursor. ( Ignorable by Annotation )

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

344
        if (!$cursor->/** @scrutinizer ignore-call */ count()) {

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...
345
            throw new UnknownVersionException($version);
346
        }
347
348
        if ($cursor->count() > 1) {
349
            throw new \DomainException(
350
                'Unexpected duplicate version records in the database'
351
            );
352
        }
353
354
        $returnVersion = $cursor->getNext();
0 ignored issues
show
Bug introduced by
The method getNext() does not exist on MongoDB\Driver\Cursor. ( Ignorable by Annotation )

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

354
        /** @scrutinizer ignore-call */ 
355
        $returnVersion = $cursor->getNext();

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...
355
356
        // Convert to normalised timestamp
357
        $ts = new Timestamp($returnVersion['t']);
358
359
        return $ts->getTimestamp();
360
    }
361
362
    /**
363
     * Return all migrated versions from versions collection that have migration files deleted.
364
     *
365
     * @return array
366
     */
367 1
    public function getUnavailableMigratedVersions()
368
    {
369 1
        return array_diff($this->getMigratedVersions(), $this->getAvailableVersions());
370
    }
371
372
    /**
373
     * @param string $name
374
     */
375 3
    public function setName($name)
376
    {
377 3
        $this->name = $name;
378
379 3
        return $this;
380
    }
381
382
    /**
383
     * @return string $name
384
     */
385 2
    public function getName()
386
    {
387 2
        return ($this->name) ?: 'Database Migrations';
388
    }
389
390
    /**
391
     * @return int
392
     */
393 1
    public function getNumberOfAvailableMigrations()
394
    {
395 1
        return count($this->migrations);
396
    }
397
398
    /**
399
     * @return int
400
     */
401
    public function getNumberOfExecutedMigrations()
402
    {
403
        $this->createMigrationCollection();
404
405
        $cursor = $this->getCollection()->find();
406
407
        return $cursor->count();
408
    }
409
410
    /**
411
     * @return \AntiMattr\MongoDB\Migrations\OutputWriter
412
     */
413 4
    public function getOutputWriter()
414
    {
415 4
        return $this->outputWriter;
416
    }
417
418
    /**
419
     * Register a single migration version to be executed by a AbstractMigration
420
     * class.
421
     *
422
     * @param string $version The version of the migration in the format YYYYMMDDHHMMSS
423
     * @param string $class   The migration class to execute for the version
424
     *
425
     * @return Version
426
     *
427
     * @throws AntiMattr\MongoDB\Migrations\Exception\DuplicateVersionException
428
     */
429 1
    public function registerMigration($version, $class)
430
    {
431 1
        $version = (string) $version;
432 1
        $class = (string) $class;
433 1
        if (isset($this->migrations[$version])) {
434
            $message = sprintf(
435
                'Migration version %s already registered with class %s',
436
                $version,
437
                get_class($this->migrations[$version])
438
            );
439
            throw new DuplicateVersionException($message);
440
        }
441 1
        $version = new Version($this, $version, $class);
442 1
        $this->migrations[$version->getVersion()] = $version;
443 1
        ksort($this->migrations);
444
445 1
        return $version;
446
    }
447
448
    /**
449
     * Register an array of migrations. Each key of the array is the version and
450
     * the value is the migration class name.
451
     *
452
     *
453
     * @param array $migrations
454
     *
455
     * @return Version[]
456
     */
457
    public function registerMigrations(array $migrations)
458
    {
459
        $versions = [];
460
        foreach ($migrations as $version => $class) {
461
            $versions[] = $this->registerMigration($version, $class);
462
        }
463
464
        return $versions;
465
    }
466
467
    /**
468
     * Register migrations from a given directory. Recursively finds all files
469
     * with the pattern VersionYYYYMMDDHHMMSS.php as the filename and registers
470
     * them as migrations.
471
     *
472
     * @param string $path The root directory to where some migration classes live
473
     *
474
     * @return Version[] The array of migrations registered
475
     */
476 3
    public function registerMigrationsFromDirectory($path)
477
    {
478 3
        $path = realpath($path);
479 3
        $path = rtrim($path, '/');
480 3
        $files = glob($path . '/Version*.php');
481 3
        $versions = [];
482 3
        if ($files) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $files of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
483 1
            foreach ($files as $file) {
484 1
                require_once $file;
485 1
                $info = pathinfo($file);
486 1
                $version = substr($info['filename'], 7);
487 1
                $class = $this->migrationsNamespace . '\\' . $info['filename'];
488 1
                $versions[] = $this->registerMigration($version, $class);
489
            }
490
        }
491
492 3
        return $versions;
493
    }
494
495
    /**
496
     * Returns the Version instance for a given version in the format YYYYMMDDHHMMSS.
497
     *
498
     * @param string $version The version string in the format YYYYMMDDHHMMSS
499
     *
500
     * @return \AntiMattr\MongoDB\Migrations\Version
501
     *
502
     * @throws AntiMattr\MongoDB\Migrations\Exception\UnknownVersionException Throws exception if migration version does not exist
503
     */
504 2
    public function getVersion($version)
505
    {
506 2
        if (!isset($this->migrations[$version])) {
507 1
            throw new UnknownVersionException($version);
508
        }
509
510 1
        return $this->migrations[$version];
511
    }
512
513
    /**
514
     * Check if a version exists.
515
     *
516
     * @param string $version
517
     *
518
     * @return bool
519
     */
520 1
    public function hasVersion($version)
521
    {
522 1
        return isset($this->migrations[$version]);
523
    }
524
525
    /**
526
     * Check if a version has been migrated or not yet.
527
     *
528
     * @param \AntiMattr\MongoDB\Migrations\Version $version
529
     *
530
     * @return bool
531
     */
532 1
    public function hasVersionMigrated(Version $version)
533
    {
534 1
        $this->createMigrationCollection();
535
536 1
        $record = $this->getCollection()->findOne(['v' => $version->getVersion()]);
537
538 1
        return null !== $record;
539
    }
540
541
    /**
542
     * @return string
543
     */
544
    public function getCurrentVersion()
545
    {
546
        $this->createMigrationCollection();
547
548
        $migratedVersions = [];
549
        if (!empty($this->migrations)) {
550
            foreach ($this->migrations as $migration) {
551
                $migratedVersions[] = $migration->getVersion();
552
            }
553
        }
554
555
        $cursor = $this->getCollection()
556
            ->find(
557
                ['v' => ['$in' => $migratedVersions]],
558
                ['sort' => ['v' => -1], 'limit' => 1]
559
            );
560
561
        if (0 === \count($cursor->toArray())) {
562
            return '0';
563
        }
564
565
        $version = $cursor->getNext();
566
567
        return $version['v'];
568
    }
569
570
    /**
571
     * Returns the latest available migration version.
572
     *
573
     * @return string The version string in the format YYYYMMDDHHMMSS
574
     */
575
    public function getLatestVersion()
576
    {
577
        $versions = array_keys($this->migrations);
578
        $latest = end($versions);
579
580
        return false !== $latest ? (string) $latest : '0';
581
    }
582
583
    /**
584
     * Create the migration collection to track migrations with.
585
     *
586
     * @return bool Whether or not the collection was created
587
     */
588 2
    public function createMigrationCollection()
589
    {
590 2
        $this->validate();
591
592 2
        if (true !== $this->migrationCollectionCreated) {
593 2
            $collection = $this->getCollection();
594 2
            $collection->createIndex(['v' => -1], ['name' => 'version', 'unique' => true]);
595 2
            $this->migrationCollectionCreated = true;
596
        }
597
598 2
        return true;
599
    }
600
601
    /**
602
     * Returns the array of migrations to executed based on the given direction
603
     * and target version number.
604
     *
605
     * @param string $direction The direction we are migrating
606
     * @param string $to        The version to migrate to
607
     *
608
     * @return Version[] $migrations   The array of migrations we can execute
609
     */
610
    public function getMigrationsToExecute($direction, $to)
611
    {
612
        if ('down' === $direction) {
613
            if (count($this->migrations)) {
614
                $allVersions = array_reverse(array_keys($this->migrations));
615
                $classes = array_reverse(array_values($this->migrations));
616
                $allVersions = array_combine($allVersions, $classes);
617
            } else {
618
                $allVersions = [];
619
            }
620
        } else {
621
            $allVersions = $this->migrations;
622
        }
623
        $versions = [];
624
        $migrated = $this->getMigratedVersions();
625
        foreach ($allVersions as $version) {
626
            if ($this->shouldExecuteMigration($direction, $version, $to, $migrated)) {
627
                $versions[$version->getVersion()] = $version;
628
            }
629
        }
630
631
        return $versions;
632
    }
633
634
    /**
635
     * Check if we should execute a migration for a given direction and target
636
     * migration version.
637
     *
638
     * @param string  $direction The direction we are migrating
639
     * @param Version $version   The Version instance to check
640
     * @param string  $to        The version we are migrating to
641
     * @param array   $migrated  Migrated versions array
642
     *
643
     * @return bool
644
     */
645
    private function shouldExecuteMigration($direction, Version $version, $to, $migrated)
646
    {
647
        if ('down' === $direction) {
648
            if (!in_array($version->getVersion(), $migrated)) {
649
                return false;
650
            }
651
652
            return $version->getVersion() > $to;
653
        }
654
655
        if ('up' === $direction) {
656
            if (in_array($version->getVersion(), $migrated)) {
657
                return false;
658
            }
659
660
            return $version->getVersion() <= $to;
661
        }
662
    }
663
664
    /**
665
     * Validation that this instance has all the required properties configured.
666
     *
667
     * @throws AntiMattr\MongoDB\Migrations\Exception\ConfigurationValidationException
668
     */
669 4
    public function validate()
670
    {
671 4
        if (!$this->migrationsDatabaseName) {
672 1
            $message = 'Migrations Database Name must be configured in order to use AntiMattr migrations.';
673 1
            throw new ConfigurationValidationException($message);
674
        }
675 3
        if (!$this->migrationsNamespace) {
676
            $message = 'Migrations namespace must be configured in order to use AntiMattr migrations.';
677
            throw new ConfigurationValidationException($message);
678
        }
679 3
        if (!$this->migrationsDirectory) {
680
            $message = 'Migrations directory must be configured in order to use AntiMattr migrations.';
681
            throw new ConfigurationValidationException($message);
682
        }
683 3
    }
684
685
    /**
686
     * @return array
687
     */
688
    public function getDetailsMap()
689
    {
690
        // Executed migration count
691
        $executedMigrations = $this->getMigratedVersions();
692
        $numExecutedMigrations = count($executedMigrations);
693
694
        // Available migration count
695
        $availableMigrations = $this->getAvailableVersions();
696
        $numAvailableMigrations = count($availableMigrations);
697
698
        // Executed Unavailable migration count
699
        $numExecutedUnavailableMigrations = count($this->getUnavailableMigratedVersions());
700
701
        // New migration count
702
        $numNewMigrations = $numAvailableMigrations - ($numExecutedMigrations - $numExecutedUnavailableMigrations);
703
704
        return [
705
            'name' => $this->getName(),
706
            'database_driver' => 'MongoDB',
707
            'migrations_database_name' => $this->getMigrationsDatabaseName(),
708
            'migrations_collection_name' => $this->getMigrationsCollectionName(),
709
            'migrations_namespace' => $this->getMigrationsNamespace(),
710
            'migrations_directory' => $this->getMigrationsDirectory(),
711
            'current_version' => $this->getCurrentVersion(),
712
            'latest_version' => $this->getLatestVersion(),
713
            'num_executed_migrations' => $numExecutedMigrations,
714
            'num_executed_unavailable_migrations' => $numExecutedUnavailableMigrations,
715
            'num_available_migrations' => $numAvailableMigrations,
716
            'num_new_migrations' => $numNewMigrations,
717
        ];
718
    }
719
}
720