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

Configuration::getMigratedTimestamp()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3.004

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 3
nop 1
dl 0
loc 25
ccs 12
cts 13
cp 0.9231
crap 3.004
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 17
    public function __construct(Client $connection, OutputWriter $outputWriter = null)
110
    {
111 17
        $this->connection = $connection;
112 17
        if (null === $outputWriter) {
113 14
            $outputWriter = new OutputWriter();
114
        }
115 17
        $this->outputWriter = $outputWriter;
116 17
    }
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 7
    public function getCollection()
156
    {
157 7
        if (isset($this->collection)) {
158 6
            return $this->collection;
159
        }
160
161 7
        $this->collection = $this->getDatabase()->selectCollection($this->migrationsCollectionName);
162
163 7
        return $this->collection;
164
    }
165
166
    /**
167
     * @return \MongoDB\Client
168
     */
169 4
    public function getConnection()
170
    {
171 4
        return $this->connection;
172
    }
173
174
    /**
175
     * @return \MongoDB\Database
176
     */
177 9
    public function getDatabase(): ?\MongoDB\Database
178
    {
179 9
        if (isset($this->database)) {
180
            return $this->database;
181
        }
182
183 9
        $this->database = $this->connection->selectDatabase($this->migrationsDatabaseName);
184
185 9
        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 12
    public function setMigrationsDatabaseName($databaseName)
202
    {
203 12
        $this->migrationsDatabaseName = $databaseName;
204
205 12
        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 11
    public function setMigrationsCollectionName($collectionName)
220
    {
221 11
        $this->migrationsCollectionName = $collectionName;
222
223 11
        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 9
    public function setMigrationsDirectory($migrationsDirectory)
238
    {
239 9
        $this->migrationsDirectory = $migrationsDirectory;
240
241 9
        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 11
    public function setMigrationsNamespace($migrationsNamespace)
258
    {
259 11
        $this->migrationsNamespace = $migrationsNamespace;
260
261 11
        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 2
    public function getMigratedTimestamp($version): int
337
    {
338 2
        $this->createMigrationCollection();
339
340 2
        $cursor = $this->getCollection()->find(
341 2
            ['v' => $version]
342
        );
343
344 2
        $result = $cursor->toArray();
345 2
        if (!count($result)) {
346
            throw new UnknownVersionException($version);
347
        }
348
349 2
        if (count($result) > 1) {
350 1
            throw new \DomainException(
351 1
                'Unexpected duplicate version records in the database'
352
            );
353
        }
354
355 1
        $returnVersion = $result[0];
356
357
        // Convert to normalised timestamp
358 1
        $ts = new Timestamp($returnVersion['t']);
359
360 1
        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 1
    public function getNumberOfExecutedMigrations()
403
    {
404 1
        $this->createMigrationCollection();
405
406 1
        return $this->getCollection()->countDocuments();
407
    }
408
409
    /**
410
     * @return \AntiMattr\MongoDB\Migrations\OutputWriter
411
     */
412 5
    public function getOutputWriter()
413
    {
414 5
        return $this->outputWriter;
415
    }
416
417
    /**
418
     * Register a single migration version to be executed by a AbstractMigration
419
     * class.
420
     *
421
     * @param string $version The version of the migration in the format YYYYMMDDHHMMSS
422
     * @param string $class   The migration class to execute for the version
423
     *
424
     * @return Version
425
     *
426
     * @throws AntiMattr\MongoDB\Migrations\Exception\DuplicateVersionException
427
     */
428 2
    public function registerMigration($version, $class)
429
    {
430 2
        $version = (string) $version;
431 2
        $class = (string) $class;
432 2
        if (isset($this->migrations[$version])) {
433
            $message = sprintf(
434
                'Migration version %s already registered with class %s',
435
                $version,
436
                get_class($this->migrations[$version])
437
            );
438
            throw new DuplicateVersionException($message);
439
        }
440 2
        $version = new Version($this, $version, $class);
441 2
        $this->migrations[$version->getVersion()] = $version;
442 2
        ksort($this->migrations);
443
444 2
        return $version;
445
    }
446
447
    /**
448
     * Register an array of migrations. Each key of the array is the version and
449
     * the value is the migration class name.
450
     *
451
     *
452
     * @param array $migrations
453
     *
454
     * @return Version[]
455
     */
456
    public function registerMigrations(array $migrations)
457
    {
458
        $versions = [];
459
        foreach ($migrations as $version => $class) {
460
            $versions[] = $this->registerMigration($version, $class);
461
        }
462
463
        return $versions;
464
    }
465
466
    /**
467
     * Register migrations from a given directory. Recursively finds all files
468
     * with the pattern VersionYYYYMMDDHHMMSS.php as the filename and registers
469
     * them as migrations.
470
     *
471
     * @param string $path The root directory to where some migration classes live
472
     *
473
     * @return Version[] The array of migrations registered
474
     */
475 4
    public function registerMigrationsFromDirectory($path)
476
    {
477 4
        $path = realpath($path);
478 4
        $path = rtrim($path, '/');
479 4
        $files = glob($path . '/Version*.php');
480 4
        $versions = [];
481 4
        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...
482 2
            foreach ($files as $file) {
483 2
                require_once $file;
484 2
                $info = pathinfo($file);
485 2
                $version = substr($info['filename'], 7);
486 2
                $class = $this->migrationsNamespace . '\\' . $info['filename'];
487 2
                $versions[] = $this->registerMigration($version, $class);
488
            }
489
        }
490
491 4
        return $versions;
492
    }
493
494
    /**
495
     * Returns the Version instance for a given version in the format YYYYMMDDHHMMSS.
496
     *
497
     * @param string $version The version string in the format YYYYMMDDHHMMSS
498
     *
499
     * @return \AntiMattr\MongoDB\Migrations\Version
500
     *
501
     * @throws AntiMattr\MongoDB\Migrations\Exception\UnknownVersionException Throws exception if migration version does not exist
502
     */
503 2
    public function getVersion($version)
504
    {
505 2
        if (!isset($this->migrations[$version])) {
506 1
            throw new UnknownVersionException($version);
507
        }
508
509 1
        return $this->migrations[$version];
510
    }
511
512
    /**
513
     * Check if a version exists.
514
     *
515
     * @param string $version
516
     *
517
     * @return bool
518
     */
519 1
    public function hasVersion($version)
520
    {
521 1
        return isset($this->migrations[$version]);
522
    }
523
524
    /**
525
     * Check if a version has been migrated or not yet.
526
     *
527
     * @param \AntiMattr\MongoDB\Migrations\Version $version
528
     *
529
     * @return bool
530
     */
531 1
    public function hasVersionMigrated(Version $version)
532
    {
533 1
        $this->createMigrationCollection();
534
535 1
        $record = $this->getCollection()->findOne(['v' => $version->getVersion()]);
536
537 1
        return null !== $record;
538
    }
539
540
    /**
541
     * @return string
542
     */
543 1
    public function getCurrentVersion()
544
    {
545 1
        $this->createMigrationCollection();
546
547 1
        $migratedVersions = [];
548 1
        if (!empty($this->migrations)) {
549 1
            foreach ($this->migrations as $migration) {
550 1
                $migratedVersions[] = $migration->getVersion();
551
            }
552
        }
553
554 1
        $cursor = $this->getCollection()
555 1
            ->find(
556 1
                ['v' => ['$in' => $migratedVersions]],
557 1
                ['sort' => ['v' => -1], 'limit' => 1]
558
            );
559
560 1
        $versions = $cursor->toArray();
561 1
        if (0 === \count($versions)) {
562
            return '0';
563
        }
564
565 1
        $version = $versions[0];
566 1
        return $version['v'];
567
    }
568
569
    /**
570
     * Returns the latest available migration version.
571
     *
572
     * @return string The version string in the format YYYYMMDDHHMMSS
573
     */
574
    public function getLatestVersion()
575
    {
576
        $versions = array_keys($this->migrations);
577
        $latest = end($versions);
578
579
        return false !== $latest ? (string) $latest : '0';
580
    }
581
582
    /**
583
     * Create the migration collection to track migrations with.
584
     *
585
     * @return bool Whether or not the collection was created
586
     */
587 6
    public function createMigrationCollection()
588
    {
589 6
        $this->validate();
590
591 6
        if (true !== $this->migrationCollectionCreated) {
592 6
            $collection = $this->getCollection();
593 6
            $collection->createIndex(['v' => -1], ['name' => 'version', 'unique' => true]);
594 6
            $this->migrationCollectionCreated = true;
595
        }
596
597 6
        return true;
598
    }
599
600
    /**
601
     * Returns the array of migrations to executed based on the given direction
602
     * and target version number.
603
     *
604
     * @param string $direction The direction we are migrating
605
     * @param string $to        The version to migrate to
606
     *
607
     * @return Version[] $migrations   The array of migrations we can execute
608
     */
609
    public function getMigrationsToExecute($direction, $to)
610
    {
611
        if ('down' === $direction) {
612
            if (count($this->migrations)) {
613
                $allVersions = array_reverse(array_keys($this->migrations));
614
                $classes = array_reverse(array_values($this->migrations));
615
                $allVersions = array_combine($allVersions, $classes);
616
            } else {
617
                $allVersions = [];
618
            }
619
        } else {
620
            $allVersions = $this->migrations;
621
        }
622
        $versions = [];
623
        $migrated = $this->getMigratedVersions();
624
        foreach ($allVersions as $version) {
625
            if ($this->shouldExecuteMigration($direction, $version, $to, $migrated)) {
626
                $versions[$version->getVersion()] = $version;
627
            }
628
        }
629
630
        return $versions;
631
    }
632
633
    /**
634
     * Check if we should execute a migration for a given direction and target
635
     * migration version.
636
     *
637
     * @param string  $direction The direction we are migrating
638
     * @param Version $version   The Version instance to check
639
     * @param string  $to        The version we are migrating to
640
     * @param array   $migrated  Migrated versions array
641
     *
642
     * @return bool
643
     */
644
    private function shouldExecuteMigration($direction, Version $version, $to, $migrated)
645
    {
646
        if ('down' === $direction) {
647
            if (!in_array($version->getVersion(), $migrated)) {
648
                return false;
649
            }
650
651
            return $version->getVersion() > $to;
652
        }
653
654
        if ('up' === $direction) {
655
            if (in_array($version->getVersion(), $migrated)) {
656
                return false;
657
            }
658
659
            return $version->getVersion() <= $to;
660
        }
661
    }
662
663
    /**
664
     * Validation that this instance has all the required properties configured.
665
     *
666
     * @throws AntiMattr\MongoDB\Migrations\Exception\ConfigurationValidationException
667
     */
668 8
    public function validate()
669
    {
670 8
        if (!$this->migrationsDatabaseName) {
671 1
            $message = 'Migrations Database Name must be configured in order to use AntiMattr migrations.';
672 1
            throw new ConfigurationValidationException($message);
673
        }
674 7
        if (!$this->migrationsNamespace) {
675
            $message = 'Migrations namespace must be configured in order to use AntiMattr migrations.';
676
            throw new ConfigurationValidationException($message);
677
        }
678 7
        if (!$this->migrationsDirectory) {
679
            $message = 'Migrations directory must be configured in order to use AntiMattr migrations.';
680
            throw new ConfigurationValidationException($message);
681
        }
682 7
    }
683
684
    /**
685
     * @return array
686
     */
687
    public function getDetailsMap()
688
    {
689
        // Executed migration count
690
        $executedMigrations = $this->getMigratedVersions();
691
        $numExecutedMigrations = count($executedMigrations);
692
693
        // Available migration count
694
        $availableMigrations = $this->getAvailableVersions();
695
        $numAvailableMigrations = count($availableMigrations);
696
697
        // Executed Unavailable migration count
698
        $numExecutedUnavailableMigrations = count($this->getUnavailableMigratedVersions());
699
700
        // New migration count
701
        $numNewMigrations = $numAvailableMigrations - ($numExecutedMigrations - $numExecutedUnavailableMigrations);
702
703
        return [
704
            'name' => $this->getName(),
705
            'database_driver' => 'MongoDB',
706
            'migrations_database_name' => $this->getMigrationsDatabaseName(),
707
            'migrations_collection_name' => $this->getMigrationsCollectionName(),
708
            'migrations_namespace' => $this->getMigrationsNamespace(),
709
            'migrations_directory' => $this->getMigrationsDirectory(),
710
            'current_version' => $this->getCurrentVersion(),
711
            'latest_version' => $this->getLatestVersion(),
712
            'num_executed_migrations' => $numExecutedMigrations,
713
            'num_executed_unavailable_migrations' => $numExecutedUnavailableMigrations,
714
            'num_available_migrations' => $numAvailableMigrations,
715
            'num_new_migrations' => $numNewMigrations,
716
        ];
717
    }
718
}
719