Completed
Push — master ( 1b8fb3...8ace2b )
by Douglas
11s
created

Configuration::getMigratedTimestamp()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3.0052

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 3
nop 1
dl 0
loc 24
ccs 11
cts 12
cp 0.9167
crap 3.0052
rs 8.9713
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 Doctrine\MongoDB\Connection;
20
use Doctrine\MongoDB\Database;
21
22
/**
23
 * @author Matthew Fitzgerald <[email protected]>
24
 */
25
class Configuration
26
{
27
    /**
28
     * @var Doctrine\MongoDB\Collection
0 ignored issues
show
Bug introduced by
The type AntiMattr\MongoDB\Migrat...rine\MongoDB\Collection was not found. Did you mean Doctrine\MongoDB\Collection? If so, make sure to prefix the type with \.
Loading history...
29
     */
30
    private $collection;
31
32
    /**
33
     * @var Doctrine\MongoDB\Connection
0 ignored issues
show
Bug introduced by
The type AntiMattr\MongoDB\Migrat...rine\MongoDB\Connection was not found. Did you mean Doctrine\MongoDB\Connection? If so, make sure to prefix the type with \.
Loading history...
34
     */
35
    private $connection;
36
37
    /**
38
     * @var Doctrine\MongoDB\Database
0 ignored issues
show
Bug introduced by
The type AntiMattr\MongoDB\Migrat...ctrine\MongoDB\Database was not found. Did you mean Doctrine\MongoDB\Database? If so, make sure to prefix the type with \.
Loading history...
39
     */
40
    private $database;
41
42
    /**
43
     * @var Doctrine\MongoDB\Connection
44
     */
45
    private $migrationsDatabase;
0 ignored issues
show
introduced by
The private property $migrationsDatabase is not used, and could be removed.
Loading history...
46
47
    /**
48
     * The migration database name to track versions in.
49
     *
50
     * @var string
51
     */
52
    private $migrationsDatabaseName;
53
54
    /**
55
     * Flag for whether or not the migration collection has been created.
56
     *
57
     * @var bool
58
     */
59
    private $migrationCollectionCreated = false;
60
61
    /**
62
     * The migration collection name to track versions in.
63
     *
64
     * @var string
65
     */
66
    private $migrationsCollectionName = 'antimattr_migration_versions';
67
68
    /**
69
     * The path to a directory where new migration classes will be written.
70
     *
71
     * @var string
72
     */
73
    private $migrationsDirectory;
74
75
    /**
76
     * Namespace the migration classes live in.
77
     *
78
     * @var string
79
     */
80
    private $migrationsNamespace;
81
82
    /**
83
     * The path to a directory where mongo console scripts are.
84
     *
85
     * @var string
86
     */
87
    private $migrationsScriptDirectory;
88
89
    /**
90
     * Used by Console Commands and Output Writer.
91
     *
92
     * @var string
93
     */
94
    private $name;
95
96
    /**
97
     * @var AntiMattr\MongoDB\Migrations\Version[]
98
     */
99
    protected $migrations = [];
100
101
    /**
102
     * @var AntiMattr\MongoDB\Migrations\OutputWriter
0 ignored issues
show
Bug introduced by
The type AntiMattr\MongoDB\Migrat...Migrations\OutputWriter was not found. Did you mean AntiMattr\MongoDB\Migrations\OutputWriter? If so, make sure to prefix the type with \.
Loading history...
103
     */
104
    private $outputWriter;
105
106
    /**
107
     * @param Doctrine\MongoDB\Connection               $connection
108
     * @param AntiMattr\MongoDB\Migrations\OutputWriter $outputWriter
109
     */
110 26
    public function __construct(Connection $connection, OutputWriter $outputWriter = null)
111
    {
112 26
        $this->connection = $connection;
0 ignored issues
show
Documentation Bug introduced by
It seems like $connection of type Doctrine\MongoDB\Connection is incompatible with the declared type AntiMattr\MongoDB\Migrat...rine\MongoDB\Connection of property $connection.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
113 26
        if (null === $outputWriter) {
114 26
            $outputWriter = new OutputWriter();
115
        }
116 26
        $this->outputWriter = $outputWriter;
0 ignored issues
show
Documentation Bug introduced by
It seems like $outputWriter of type AntiMattr\MongoDB\Migrations\OutputWriter is incompatible with the declared type AntiMattr\MongoDB\Migrat...Migrations\OutputWriter of property $outputWriter.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
117 26
    }
118
119
    /**
120
     * Returns a timestamp version as a formatted date.
121
     *
122
     * @param string $version
123
     *
124
     * @return string The formatted version
125
     */
126 3
    public static function formatVersion($version)
127
    {
128 3
        return sprintf('%s-%s-%s %s:%s:%s',
129 3
            substr($version, 0, 4),
130 3
            substr($version, 4, 2),
131 3
            substr($version, 6, 2),
132 3
            substr($version, 8, 2),
133 3
            substr($version, 10, 2),
134 3
            substr($version, 12, 2)
135
        );
136
    }
137
138
    /**
139
     * Returns an array of available migration version numbers.
140
     *
141
     * @return array
142
     */
143 2
    public function getAvailableVersions()
144
    {
145 2
        $availableVersions = [];
146 2
        foreach ($this->migrations as $migration) {
147 1
            $availableVersions[] = $migration->getVersion();
148
        }
149
150 2
        return $availableVersions;
151
    }
152
153
    /**
154
     * @return Doctrine\MongoDB\Collection
155
     */
156 7
    public function getCollection()
157
    {
158 7
        if (isset($this->collection)) {
159 6
            return $this->collection;
160
        }
161
162 7
        $this->collection = $this->getDatabase()->selectCollection($this->migrationsCollectionName);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getDatabase()->se...grationsCollectionName) of type Doctrine\MongoDB\Collection is incompatible with the declared type AntiMattr\MongoDB\Migrat...rine\MongoDB\Collection of property $collection.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
163
164 7
        return $this->collection;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->collection returns the type Doctrine\MongoDB\Collection which is incompatible with the documented return type AntiMattr\MongoDB\Migrat...rine\MongoDB\Collection.
Loading history...
165
    }
166
167
    /**
168
     * @return Doctrine\MongoDB\Connection
169
     */
170 3
    public function getConnection()
171
    {
172 3
        return $this->connection;
173
    }
174
175
    /**
176
     * @return Doctrine\MongoDB\Database
177
     */
178 9
    public function getDatabase(): ?Database
179
    {
180 9
        if (isset($this->database)) {
181
            return $this->database;
182
        }
183
184 9
        $this->database = $this->connection->selectDatabase($this->migrationsDatabaseName);
185
186 9
        return $this->database;
187
    }
188
189
    /**
190
     * Get the array of registered migration versions.
191
     *
192
     * @return Version[] $migrations
193
     */
194 2
    public function getMigrations()
195
    {
196 2
        return $this->migrations;
197
    }
198
199
    /**
200
     * @param string $databaseName
201
     */
202 21
    public function setMigrationsDatabaseName($databaseName)
203
    {
204 21
        $this->migrationsDatabaseName = $databaseName;
205 21
    }
206
207
    /**
208
     * @return string
209
     */
210 4
    public function getMigrationsDatabaseName()
211
    {
212 4
        return $this->migrationsDatabaseName;
213
    }
214
215
    /**
216
     * @param string $collectionName
217
     */
218 20
    public function setMigrationsCollectionName($collectionName)
219
    {
220 20
        $this->migrationsCollectionName = $collectionName;
221 20
    }
222
223
    /**
224
     * @return string
225
     */
226 2
    public function getMigrationsCollectionName()
227
    {
228 2
        return $this->migrationsCollectionName;
229
    }
230
231
    /**
232
     * @param string $migrationsDirectory
233
     */
234 19
    public function setMigrationsDirectory($migrationsDirectory)
235
    {
236 19
        $this->migrationsDirectory = $migrationsDirectory;
237 19
    }
238
239
    /**
240
     * @return string
241
     */
242 2
    public function getMigrationsDirectory()
243
    {
244 2
        return $this->migrationsDirectory;
245
    }
246
247
    /**
248
     * Set the migrations namespace.
249
     *
250
     * @param string $migrationsNamespace The migrations namespace
251
     */
252 20
    public function setMigrationsNamespace($migrationsNamespace)
253
    {
254 20
        $this->migrationsNamespace = $migrationsNamespace;
255 20
    }
256
257
    /**
258
     * @return string $migrationsNamespace
259
     */
260 2
    public function getMigrationsNamespace()
261
    {
262 2
        return $this->migrationsNamespace;
263
    }
264
265
    /**
266
     * @param string $scriptsDirectory
267
     */
268 12
    public function setMigrationsScriptDirectory($scriptsDirectory)
269
    {
270 12
        $this->migrationsScriptDirectory = $scriptsDirectory;
271 12
    }
272
273
    /**
274
     * @return string
275
     */
276 2
    public function getMigrationsScriptDirectory()
277
    {
278 2
        return $this->migrationsScriptDirectory;
279
    }
280
281
    /**
282
     * Returns all migrated versions from the versions collection, in an array.
283
     *
284
     * @return AntiMattr\MongoDB\Migrations\Version[]
285
     */
286 1
    public function getMigratedVersions()
287
    {
288 1
        $this->createMigrationCollection();
289
290 1
        $cursor = $this->getCollection()->find();
291 1
        $versions = [];
292 1
        foreach ($cursor as $record) {
293 1
            $versions[] = $record['v'];
294
        }
295
296 1
        return $versions;
297
    }
298
299
    /**
300
     * Returns the time a migration occurred.
301
     *
302
     * @param string $version
303
     *
304
     * @return string
305
     *
306
     * @throws AntiMattr\MongoDB\Migrations\Exception\UnknownVersionException Throws exception if migration version does not exist
307
     * @throws DomainException                                                If more than one version exists
308
     */
309 2
    public function getMigratedTimestamp($version)
310
    {
311 2
        $this->createMigrationCollection();
312
313 2
        $cursor = $this->getCollection()->find(
314 2
            ['v' => $version]
315
        );
316
317 2
        if (!$cursor->count()) {
318
            throw new UnknownVersionException($version);
319
        }
320
321 2
        if ($cursor->count() > 1) {
322 1
            throw new \DomainException(
323 1
                'Unexpected duplicate version records in the database'
324
            );
325
        }
326
327 1
        $returnVersion = $cursor->getNext();
328
329
        // Convert to normalised timestamp
330 1
        $ts = new Timestamp($returnVersion['t']);
331
332 1
        return (string) $ts;
333
    }
334
335
    /**
336
     * Return all migrated versions from versions collection that have migration files deleted.
337
     *
338
     * @return array
339
     */
340 1
    public function getUnavailableMigratedVersions()
341
    {
342 1
        return array_diff($this->getMigratedVersions(), $this->getAvailableVersions());
343
    }
344
345
    /**
346
     * @param string $name
347
     */
348 12
    public function setName($name)
349
    {
350 12
        $this->name = $name;
351 12
    }
352
353
    /**
354
     * @return string $name
355
     */
356 2
    public function getName()
357
    {
358 2
        return ($this->name) ?: 'Database Migrations';
359
    }
360
361
    /**
362
     * @return int
363
     */
364 1
    public function getNumberOfAvailableMigrations()
365
    {
366 1
        return count($this->migrations);
367
    }
368
369
    /**
370
     * @return int
371
     */
372 1
    public function getNumberOfExecutedMigrations()
373
    {
374 1
        $this->createMigrationCollection();
375
376 1
        $cursor = $this->getCollection()->find();
377
378 1
        return $cursor->count();
379
    }
380
381
    /**
382
     * @return AntiMattr\MongoDB\Migrations\OutputWriter
383
     */
384 2
    public function getOutputWriter()
385
    {
386 2
        return $this->outputWriter;
387
    }
388
389
    /**
390
     * Register a single migration version to be executed by a AbstractMigration
391
     * class.
392
     *
393
     * @param string $version The version of the migration in the format YYYYMMDDHHMMSS
394
     * @param string $class   The migration class to execute for the version
395
     *
396
     * @return Version
397
     *
398
     * @throws AntiMattr\MongoDB\Migrations\Exception\DuplicateVersionException
399
     */
400 2
    public function registerMigration($version, $class)
401
    {
402 2
        $version = (string) $version;
403 2
        $class = (string) $class;
404 2
        if (isset($this->migrations[$version])) {
405
            $message = sprintf(
406
                'Migration version %s already registered with class %s',
407
                $version,
408
                get_class($this->migrations[$version])
409
            );
410
            throw new DuplicateVersionException($message);
411
        }
412 2
        $version = new Version($this, $version, $class);
413 2
        $this->migrations[$version->getVersion()] = $version;
414 2
        ksort($this->migrations);
415
416 2
        return $version;
417
    }
418
419
    /**
420
     * Register an array of migrations. Each key of the array is the version and
421
     * the value is the migration class name.
422
     *
423
     *
424
     * @param array $migrations
425
     *
426
     * @return Version[]
427
     */
428
    public function registerMigrations(array $migrations)
429
    {
430
        $versions = [];
431
        foreach ($migrations as $version => $class) {
432
            $versions[] = $this->registerMigration($version, $class);
433
        }
434
435
        return $versions;
436
    }
437
438
    /**
439
     * Register migrations from a given directory. Recursively finds all files
440
     * with the pattern VersionYYYYMMDDHHMMSS.php as the filename and registers
441
     * them as migrations.
442
     *
443
     * @param string $path The root directory to where some migration classes live
444
     *
445
     * @return Version[] The array of migrations registered
446
     */
447 14
    public function registerMigrationsFromDirectory($path)
448
    {
449 14
        $path = realpath($path);
450 14
        $path = rtrim($path, '/');
451 14
        $files = glob($path . '/Version*.php');
452 14
        $versions = [];
453 14
        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...
454 2
            foreach ($files as $file) {
455 2
                require_once $file;
456 2
                $info = pathinfo($file);
457 2
                $version = substr($info['filename'], 7);
458 2
                $class = $this->migrationsNamespace . '\\' . $info['filename'];
459 2
                $versions[] = $this->registerMigration($version, $class);
460
            }
461
        }
462
463 14
        return $versions;
464
    }
465
466
    /**
467
     * Returns the Version instance for a given version in the format YYYYMMDDHHMMSS.
468
     *
469
     * @param string $version The version string in the format YYYYMMDDHHMMSS
470
     *
471
     * @return AntiMattr\MongoDB\Migrations\Version
0 ignored issues
show
Bug introduced by
The type AntiMattr\MongoDB\Migrat...goDB\Migrations\Version was not found. Did you mean AntiMattr\MongoDB\Migrations\Version? If so, make sure to prefix the type with \.
Loading history...
472
     *
473
     * @throws AntiMattr\MongoDB\Migrations\Exception\UnknownVersionException Throws exception if migration version does not exist
474
     */
475 2
    public function getVersion($version)
476
    {
477 2
        if (!isset($this->migrations[$version])) {
478 1
            throw new UnknownVersionException($version);
479
        }
480
481 1
        return $this->migrations[$version];
482
    }
483
484
    /**
485
     * Check if a version exists.
486
     *
487
     * @param string $version
488
     *
489
     * @return bool
490
     */
491 1
    public function hasVersion($version)
492
    {
493 1
        return isset($this->migrations[$version]);
494
    }
495
496
    /**
497
     * Check if a version has been migrated or not yet.
498
     *
499
     * @param AntiMattr\MongoDB\Migrations\Version $version
500
     *
501
     * @return bool
502
     */
503 1
    public function hasVersionMigrated(Version $version)
504
    {
505 1
        $this->createMigrationCollection();
506
507 1
        $record = $this->getCollection()->findOne(['v' => $version->getVersion()]);
508
509 1
        return null !== $record;
510
    }
511
512
    /**
513
     * @return string
514
     */
515 1
    public function getCurrentVersion()
516
    {
517 1
        $this->createMigrationCollection();
518
519 1
        $migratedVersions = [];
520 1
        if ($this->migrations) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->migrations of type AntiMattr\MongoDB\Migrat...DB\Migrations\Version[] 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...
521 1
            foreach ($this->migrations as $migration) {
522 1
                $migratedVersions[] = $migration->getVersion();
523
            }
524
        }
525
526 1
        $cursor = $this->getCollection()
527 1
            ->find(
528 1
                ['v' => ['$in' => $migratedVersions]]
529
            )
530 1
            ->sort(['v' => -1])
531 1
            ->limit(1);
532
533 1
        if (0 === $cursor->count()) {
534
            return '0';
535
        }
536
537 1
        $version = $cursor->getNext();
538
539 1
        return $version['v'];
540
    }
541
542
    /**
543
     * Returns the latest available migration version.
544
     *
545
     * @return string The version string in the format YYYYMMDDHHMMSS
546
     */
547
    public function getLatestVersion()
548
    {
549
        $versions = array_keys($this->migrations);
550
        $latest = end($versions);
551
552
        return false !== $latest ? (string) $latest : '0';
553
    }
554
555
    /**
556
     * Create the migration collection to track migrations with.
557
     *
558
     * @return bool Whether or not the collection was created
559
     */
560 6
    public function createMigrationCollection()
561
    {
562 6
        $this->validate();
563
564 6
        if (true !== $this->migrationCollectionCreated) {
565 6
            $collection = $this->getCollection();
566 6
            $collection->ensureIndex(['v' => -1], ['name' => 'version', 'unique' => true]);
567 6
            $this->migrationCollectionCreated = true;
568
        }
569
570 6
        return true;
571
    }
572
573
    /**
574
     * Returns the array of migrations to executed based on the given direction
575
     * and target version number.
576
     *
577
     * @param string $direction The direction we are migrating
578
     * @param string $to        The version to migrate to
579
     *
580
     * @return Version[] $migrations   The array of migrations we can execute
581
     */
582
    public function getMigrationsToExecute($direction, $to)
583
    {
584
        if ('down' === $direction) {
585
            if (count($this->migrations)) {
586
                $allVersions = array_reverse(array_keys($this->migrations));
587
                $classes = array_reverse(array_values($this->migrations));
588
                $allVersions = array_combine($allVersions, $classes);
589
            } else {
590
                $allVersions = [];
591
            }
592
        } else {
593
            $allVersions = $this->migrations;
594
        }
595
        $versions = [];
596
        $migrated = $this->getMigratedVersions();
597
        foreach ($allVersions as $version) {
598
            if ($this->shouldExecuteMigration($direction, $version, $to, $migrated)) {
599
                $versions[$version->getVersion()] = $version;
600
            }
601
        }
602
603
        return $versions;
604
    }
605
606
    /**
607
     * Check if we should execute a migration for a given direction and target
608
     * migration version.
609
     *
610
     * @param string  $direction The direction we are migrating
611
     * @param Version $version   The Version instance to check
612
     * @param string  $to        The version we are migrating to
613
     * @param array   $migrated  Migrated versions array
614
     *
615
     * @return bool
616
     */
617
    private function shouldExecuteMigration($direction, Version $version, $to, $migrated)
618
    {
619
        if ('down' === $direction) {
620
            if (!in_array($version->getVersion(), $migrated)) {
621
                return false;
622
            }
623
624
            return $version->getVersion() > $to;
625
        }
626
627
        if ('up' === $direction) {
628
            if (in_array($version->getVersion(), $migrated)) {
629
                return false;
630
            }
631
632
            return $version->getVersion() <= $to;
633
        }
634
    }
635
636
    /**
637
     * Validation that this instance has all the required properties configured.
638
     *
639
     * @throws AntiMattr\MongoDB\Migrations\Exception\ConfigurationValidationException
640
     */
641 8
    public function validate()
642
    {
643 8
        if (!$this->migrationsDatabaseName) {
644 1
            $message = 'Migrations Database Name must be configured in order to use AntiMattr migrations.';
645 1
            throw new ConfigurationValidationException($message);
646
        }
647 7
        if (!$this->migrationsNamespace) {
648
            $message = 'Migrations namespace must be configured in order to use AntiMattr migrations.';
649
            throw new ConfigurationValidationException($message);
650
        }
651 7
        if (!$this->migrationsDirectory) {
652
            $message = 'Migrations directory must be configured in order to use AntiMattr migrations.';
653
            throw new ConfigurationValidationException($message);
654
        }
655 7
    }
656
657
    /**
658
     * @return array
659
     */
660
    public function getDetailsMap()
661
    {
662
        // Executed migration count
663
        $executedMigrations = $this->getMigratedVersions();
664
        $numExecutedMigrations = count($executedMigrations);
665
666
        // Available migration count
667
        $availableMigrations = $this->getAvailableVersions();
668
        $numAvailableMigrations = count($availableMigrations);
669
670
        // Executed Unavailable migration count
671
        $numExecutedUnavailableMigrations = count($this->getUnavailableMigratedVersions());
672
673
        // New migration count
674
        $numNewMigrations = $numAvailableMigrations - ($numExecutedMigrations - $numExecutedUnavailableMigrations);
675
676
        return [
677
            'name' => $this->getName(),
678
            'database_driver' => 'MongoDB',
679
            'migrations_database_name' => $this->getMigrationsDatabaseName(),
680
            'migrations_collection_name' => $this->getMigrationsCollectionName(),
681
            'migrations_namespace' => $this->getMigrationsNamespace(),
682
            'migrations_directory' => $this->getMigrationsDirectory(),
683
            'current_version' => $this->getCurrentVersion(),
684
            'latest_version' => $this->getLatestVersion(),
685
            'num_executed_migrations' => $numExecutedMigrations,
686
            'num_executed_unavailable_migrations' => $numExecutedUnavailableMigrations,
687
            'num_available_migrations' => $numAvailableMigrations,
688
            'num_new_migrations' => $numNewMigrations,
689
        ];
690
    }
691
}
692