Passed
Pull Request — master (#41)
by
unknown
02:54
created

Configuration::getMigratedTimestamp()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 3
nop 1
dl 0
loc 25
ccs 0
cts 13
cp 0
crap 12
rs 9.8666
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 4
    public static function formatVersion($version)
126
    {
127 4
        return sprintf('%s-%s-%s %s:%s:%s',
128 4
            substr($version, 0, 4),
129 4
            substr($version, 4, 2),
130 4
            substr($version, 6, 2),
131 4
            substr($version, 8, 2),
132 4
            substr($version, 10, 2),
133 4
            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
        $result = $cursor->toArray();
345
        if (!count($result)) {
346
            throw new UnknownVersionException($version);
347
        }
348
349
        if (count($result) > 1) {
350
            throw new \DomainException(
351
                'Unexpected duplicate version records in the database'
352
            );
353
        }
354
355
        $returnVersion = $result[0];
356
357
        // Convert to normalised timestamp
358
        $ts = new Timestamp($returnVersion['t']);
359
360
        return $ts->getTimestamp();
361
    }
362
363
    /**
364
     * Return all migrated versions from versions collection that have migration files deleted.
365
     *
366
     * @return array
367
     */
368 1
    public function getUnavailableMigratedVersions()
369
    {
370 1
        return array_diff($this->getMigratedVersions(), $this->getAvailableVersions());
371
    }
372
373
    /**
374
     * @param string $name
375
     */
376 3
    public function setName($name)
377
    {
378 3
        $this->name = $name;
379
380 3
        return $this;
381
    }
382
383
    /**
384
     * @return string $name
385
     */
386 2
    public function getName()
387
    {
388 2
        return ($this->name) ?: 'Database Migrations';
389
    }
390
391
    /**
392
     * @return int
393
     */
394 1
    public function getNumberOfAvailableMigrations()
395
    {
396 1
        return count($this->migrations);
397
    }
398
399
    /**
400
     * @return int
401
     */
402
    public function getNumberOfExecutedMigrations()
403
    {
404
        $this->createMigrationCollection();
405
406
        $cursor = $this->getCollection()->find();
407
408
        return $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

408
        return $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...
409
    }
410
411
    /**
412
     * @return \AntiMattr\MongoDB\Migrations\OutputWriter
413
     */
414 4
    public function getOutputWriter()
415
    {
416 4
        return $this->outputWriter;
417
    }
418
419
    /**
420
     * Register a single migration version to be executed by a AbstractMigration
421
     * class.
422
     *
423
     * @param string $version The version of the migration in the format YYYYMMDDHHMMSS
424
     * @param string $class   The migration class to execute for the version
425
     *
426
     * @return Version
427
     *
428
     * @throws AntiMattr\MongoDB\Migrations\Exception\DuplicateVersionException
429
     */
430 1
    public function registerMigration($version, $class)
431
    {
432 1
        $version = (string) $version;
433 1
        $class = (string) $class;
434 1
        if (isset($this->migrations[$version])) {
435
            $message = sprintf(
436
                'Migration version %s already registered with class %s',
437
                $version,
438
                get_class($this->migrations[$version])
439
            );
440
            throw new DuplicateVersionException($message);
441
        }
442 1
        $version = new Version($this, $version, $class);
443 1
        $this->migrations[$version->getVersion()] = $version;
444 1
        ksort($this->migrations);
445
446 1
        return $version;
447
    }
448
449
    /**
450
     * Register an array of migrations. Each key of the array is the version and
451
     * the value is the migration class name.
452
     *
453
     *
454
     * @param array $migrations
455
     *
456
     * @return Version[]
457
     */
458
    public function registerMigrations(array $migrations)
459
    {
460
        $versions = [];
461
        foreach ($migrations as $version => $class) {
462
            $versions[] = $this->registerMigration($version, $class);
463
        }
464
465
        return $versions;
466
    }
467
468
    /**
469
     * Register migrations from a given directory. Recursively finds all files
470
     * with the pattern VersionYYYYMMDDHHMMSS.php as the filename and registers
471
     * them as migrations.
472
     *
473
     * @param string $path The root directory to where some migration classes live
474
     *
475
     * @return Version[] The array of migrations registered
476
     */
477 3
    public function registerMigrationsFromDirectory($path)
478
    {
479 3
        $path = realpath($path);
480 3
        $path = rtrim($path, '/');
481 3
        $files = glob($path . '/Version*.php');
482 3
        $versions = [];
483 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...
484 1
            foreach ($files as $file) {
485 1
                require_once $file;
486 1
                $info = pathinfo($file);
487 1
                $version = substr($info['filename'], 7);
488 1
                $class = $this->migrationsNamespace . '\\' . $info['filename'];
489 1
                $versions[] = $this->registerMigration($version, $class);
490
            }
491
        }
492
493 3
        return $versions;
494
    }
495
496
    /**
497
     * Returns the Version instance for a given version in the format YYYYMMDDHHMMSS.
498
     *
499
     * @param string $version The version string in the format YYYYMMDDHHMMSS
500
     *
501
     * @return \AntiMattr\MongoDB\Migrations\Version
502
     *
503
     * @throws AntiMattr\MongoDB\Migrations\Exception\UnknownVersionException Throws exception if migration version does not exist
504
     */
505 2
    public function getVersion($version)
506
    {
507 2
        if (!isset($this->migrations[$version])) {
508 1
            throw new UnknownVersionException($version);
509
        }
510
511 1
        return $this->migrations[$version];
512
    }
513
514
    /**
515
     * Check if a version exists.
516
     *
517
     * @param string $version
518
     *
519
     * @return bool
520
     */
521 1
    public function hasVersion($version)
522
    {
523 1
        return isset($this->migrations[$version]);
524
    }
525
526
    /**
527
     * Check if a version has been migrated or not yet.
528
     *
529
     * @param \AntiMattr\MongoDB\Migrations\Version $version
530
     *
531
     * @return bool
532
     */
533 1
    public function hasVersionMigrated(Version $version)
534
    {
535 1
        $this->createMigrationCollection();
536
537 1
        $record = $this->getCollection()->findOne(['v' => $version->getVersion()]);
538
539 1
        return null !== $record;
540
    }
541
542
    /**
543
     * @return string
544
     */
545
    public function getCurrentVersion()
546
    {
547
        $this->createMigrationCollection();
548
549
        $migratedVersions = [];
550
        if (!empty($this->migrations)) {
551
            foreach ($this->migrations as $migration) {
552
                $migratedVersions[] = $migration->getVersion();
553
            }
554
        }
555
556
        $cursor = $this->getCollection()
557
            ->find(
558
                ['v' => ['$in' => $migratedVersions]],
559
                ['sort' => ['v' => -1], 'limit' => 1]
560
            );
561
562
        $versions = $cursor->toArray();
563
        if (0 === \count($versions)) {
564
            return '0';
565
        }
566
567
        $version = $versions[0];
568
        return $version['v'];
569
    }
570
571
    /**
572
     * Returns the latest available migration version.
573
     *
574
     * @return string The version string in the format YYYYMMDDHHMMSS
575
     */
576
    public function getLatestVersion()
577
    {
578
        $versions = array_keys($this->migrations);
579
        $latest = end($versions);
580
581
        return false !== $latest ? (string) $latest : '0';
582
    }
583
584
    /**
585
     * Create the migration collection to track migrations with.
586
     *
587
     * @return bool Whether or not the collection was created
588
     */
589 2
    public function createMigrationCollection()
590
    {
591 2
        $this->validate();
592
593 2
        if (true !== $this->migrationCollectionCreated) {
594 2
            $collection = $this->getCollection();
595 2
            $collection->createIndex(['v' => -1], ['name' => 'version', 'unique' => true]);
596 2
            $this->migrationCollectionCreated = true;
597
        }
598
599 2
        return true;
600
    }
601
602
    /**
603
     * Returns the array of migrations to executed based on the given direction
604
     * and target version number.
605
     *
606
     * @param string $direction The direction we are migrating
607
     * @param string $to        The version to migrate to
608
     *
609
     * @return Version[] $migrations   The array of migrations we can execute
610
     */
611
    public function getMigrationsToExecute($direction, $to)
612
    {
613
        if ('down' === $direction) {
614
            if (count($this->migrations)) {
615
                $allVersions = array_reverse(array_keys($this->migrations));
616
                $classes = array_reverse(array_values($this->migrations));
617
                $allVersions = array_combine($allVersions, $classes);
618
            } else {
619
                $allVersions = [];
620
            }
621
        } else {
622
            $allVersions = $this->migrations;
623
        }
624
        $versions = [];
625
        $migrated = $this->getMigratedVersions();
626
        foreach ($allVersions as $version) {
627
            if ($this->shouldExecuteMigration($direction, $version, $to, $migrated)) {
628
                $versions[$version->getVersion()] = $version;
629
            }
630
        }
631
632
        return $versions;
633
    }
634
635
    /**
636
     * Check if we should execute a migration for a given direction and target
637
     * migration version.
638
     *
639
     * @param string  $direction The direction we are migrating
640
     * @param Version $version   The Version instance to check
641
     * @param string  $to        The version we are migrating to
642
     * @param array   $migrated  Migrated versions array
643
     *
644
     * @return bool
645
     */
646
    private function shouldExecuteMigration($direction, Version $version, $to, $migrated)
647
    {
648
        if ('down' === $direction) {
649
            if (!in_array($version->getVersion(), $migrated)) {
650
                return false;
651
            }
652
653
            return $version->getVersion() > $to;
654
        }
655
656
        if ('up' === $direction) {
657
            if (in_array($version->getVersion(), $migrated)) {
658
                return false;
659
            }
660
661
            return $version->getVersion() <= $to;
662
        }
663
    }
664
665
    /**
666
     * Validation that this instance has all the required properties configured.
667
     *
668
     * @throws AntiMattr\MongoDB\Migrations\Exception\ConfigurationValidationException
669
     */
670 4
    public function validate()
671
    {
672 4
        if (!$this->migrationsDatabaseName) {
673 1
            $message = 'Migrations Database Name must be configured in order to use AntiMattr migrations.';
674 1
            throw new ConfigurationValidationException($message);
675
        }
676 3
        if (!$this->migrationsNamespace) {
677
            $message = 'Migrations namespace must be configured in order to use AntiMattr migrations.';
678
            throw new ConfigurationValidationException($message);
679
        }
680 3
        if (!$this->migrationsDirectory) {
681
            $message = 'Migrations directory must be configured in order to use AntiMattr migrations.';
682
            throw new ConfigurationValidationException($message);
683
        }
684 3
    }
685
686
    /**
687
     * @return array
688
     */
689
    public function getDetailsMap()
690
    {
691
        // Executed migration count
692
        $executedMigrations = $this->getMigratedVersions();
693
        $numExecutedMigrations = count($executedMigrations);
694
695
        // Available migration count
696
        $availableMigrations = $this->getAvailableVersions();
697
        $numAvailableMigrations = count($availableMigrations);
698
699
        // Executed Unavailable migration count
700
        $numExecutedUnavailableMigrations = count($this->getUnavailableMigratedVersions());
701
702
        // New migration count
703
        $numNewMigrations = $numAvailableMigrations - ($numExecutedMigrations - $numExecutedUnavailableMigrations);
704
705
        return [
706
            'name' => $this->getName(),
707
            'database_driver' => 'MongoDB',
708
            'migrations_database_name' => $this->getMigrationsDatabaseName(),
709
            'migrations_collection_name' => $this->getMigrationsCollectionName(),
710
            'migrations_namespace' => $this->getMigrationsNamespace(),
711
            'migrations_directory' => $this->getMigrationsDirectory(),
712
            'current_version' => $this->getCurrentVersion(),
713
            'latest_version' => $this->getLatestVersion(),
714
            'num_executed_migrations' => $numExecutedMigrations,
715
            'num_executed_unavailable_migrations' => $numExecutedUnavailableMigrations,
716
            'num_available_migrations' => $numAvailableMigrations,
717
            'num_new_migrations' => $numNewMigrations,
718
        ];
719
    }
720
}
721