Passed
Pull Request — master (#38)
by Douglas
06:30
created

Configuration::setFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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