Passed
Pull Request — master (#56)
by
unknown
13:03
created

Configuration::getMigrationsCollectionName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
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
     */
45
    private ?string $migrationsDatabaseName=null;
46
47
    /**
48
     * Flag for whether or not the migration collection has been created.
49
     *
50
     * @var bool
51
     */
52
    private $migrationCollectionCreated = false;
53
54
    /**
55
     * The migration collection name to track versions in.
56
     *
57
     * @var string
58
     */
59
    private $migrationsCollectionName = 'antimattr_migration_versions';
60
61
    /**
62
     * The path to a directory where new migration classes will be written.
63
     *
64
     * @var string
65
     */
66
    private $migrationsDirectory;
67
68
    /**
69
     * Namespace the migration classes live in.
70
     *
71
     * @var string
72
     */
73
    private $migrationsNamespace;
74
75
    /**
76
     * The path to a directory where mongo console scripts are.
77
     *
78
     * @var string
79
     */
80
    private $migrationsScriptDirectory;
81
82
    /**
83
     * Used by Console Commands and Output Writer.
84
     *
85
     * @var string
86
     */
87
    private $name;
88
89
    /**
90
     * @var \AntiMattr\MongoDB\Migrations\Version[]
91
     */
92
    protected $migrations = [];
93
94
    /**
95
     * @var \AntiMattr\MongoDB\Migrations\OutputWriter
96
     */
97
    private $outputWriter;
98
    private ?string $file=null;
99
    private bool $dryRun=false;
100
101
    /**
102
     * @param \AntiMattr\MongoDB\Migrations\OutputWriter $outputWriter
103
     */
104
    public function __construct(Client $connection, OutputWriter $outputWriter = null)
105
    {
106
        $this->connection = $connection;
107
        if (null === $outputWriter) {
108 17
            $outputWriter = new OutputWriter();
109
        }
110 17
        $this->outputWriter = $outputWriter;
111 17
    }
112 14
113
    /**
114 17
     * Returns a timestamp version as a formatted date.
115 17
     *
116
     * @param string $version
117
     *
118
     * @return string The formatted version
119
     */
120
    public static function formatVersion($version)
121
    {
122
        return sprintf('%s-%s-%s %s:%s:%s',
123
            substr($version, 0, 4),
124 5
            substr($version, 4, 2),
125
            substr($version, 6, 2),
126 5
            substr($version, 8, 2),
127 5
            substr($version, 10, 2),
128 5
            substr($version, 12, 2)
129 5
        );
130 5
    }
131 5
132 5
    /**
133
     * Returns an array of available migration version numbers.
134
     *
135
     * @return array
136
     */
137
    public function getAvailableVersions()
138
    {
139
        $availableVersions = [];
140
        foreach ($this->migrations as $migration) {
141 2
            $availableVersions[] = $migration->getVersion();
142
        }
143 2
144 2
        return $availableVersions;
145 1
    }
146
147
    /**
148 2
     * @return \MongoDB\Collection
149
     */
150
    public function getCollection()
151
    {
152
        if (isset($this->collection)) {
153
            return $this->collection;
154 7
        }
155
156 7
        $this->collection = $this->getDatabase()->selectCollection($this->migrationsCollectionName);
157 6
158
        return $this->collection;
159
    }
160 7
161
    /**
162 7
     * @return \MongoDB\Client
163
     */
164
    public function getConnection()
165
    {
166
        return $this->connection;
167
    }
168 4
169
    /**
170 4
     * @return \MongoDB\Database
171
     */
172
    public function getDatabase(): ?\MongoDB\Database
173
    {
174
        if (isset($this->database)) {
175
            return $this->database;
176 9
        }
177
178 9
        $this->database = $this->connection->selectDatabase($this->migrationsDatabaseName);
0 ignored issues
show
Bug introduced by
It seems like $this->migrationsDatabaseName can also be of type null; however, parameter $databaseName of MongoDB\Client::selectDatabase() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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