Passed
Push — master ( f39217...bc6df6 )
by y
02:20
created

Migrator   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 123
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 16
eloc 49
c 1
b 0
f 0
dl 0
loc 123
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A glob() 0 11 2
A up() 0 18 5
A down() 0 21 6
A __construct() 0 6 1
A getMigration() 0 5 1
A getCurrent() 0 2 1
1
<?php
2
3
namespace Helix\DB;
4
5
use Helix\DB;
6
7
/**
8
 * @method static static factory(DB $db, string $dir);
9
 */
10
class Migrator {
11
12
    use FactoryTrait;
13
14
    /**
15
     * @var DB
16
     */
17
    protected $db;
18
19
    /**
20
     * @var string
21
     */
22
    protected $dir;
23
24
    /**
25
     * @var Table
26
     */
27
    protected $table;
28
29
    /**
30
     * @param DB $db
31
     * @param string $dir
32
     */
33
    public function __construct (DB $db, string $dir) {
34
        $this->db = $db;
35
        $this->dir = $dir;
36
        $this->table ??= $db['__migrations__'] ?? $db->getSchema()->createTable('__migrations__', [
37
                'sequence' => Schema::T_STRING_STRICT | Schema::I_PRIMARY
38
            ])['__migrations__'];
39
    }
40
41
    /**
42
     * Migrates down within a transaction.
43
     *
44
     * @param string $to Migration sequence identifier, or `null` to step down once.
45
     * @return false|string The resulting current sequence identifier.
46
     */
47
    public function down (string $to = null) {
48
        return $this->db->transact(function() use ($to) {
49
            $current = $this->getCurrent();
50
            // walk newest to oldest
51
            foreach (array_reverse($this->glob(), true) as $sequence => $spec) {
52
                if ($current and $to === $current) {
53
                    break;
54
                }
55
                if ($current < $sequence) {
56
                    continue;
57
                }
58
                $this->db->transact(function() use ($sequence, $spec) {
59
                    $this->getMigration($spec)->down($this->db);
60
                    $this->table->delete(['sequence' => $sequence]);
61
                });
62
                $current = $this->getCurrent();
63
                if ($to === null) {
64
                    break;
65
                }
66
            }
67
            return $current;
68
        });
69
    }
70
71
    /**
72
     * Returns the sequence identifier of the most recent upgrade.
73
     *
74
     * @return null|string
75
     */
76
    public function getCurrent (): ?string {
77
        return $this->table->select([$this->table['sequence']->max()])->getResult();
78
    }
79
80
    /**
81
     * @param array $spec
82
     * @return MigrationInterface
83
     */
84
    protected function getMigration (array $spec) {
85
        include_once "{$spec['file']}";
86
        $migration = new $spec['class'];
87
        assert($migration instanceof MigrationInterface);
88
        return $migration;
89
    }
90
91
    /**
92
     * Scans the migration directory for `<SEQUENCE>_<CLASS>.php` files.
93
     *
94
     * @return array[] [ file => spec array ]
95
     */
96
    protected function glob () {
97
        $specs = [];
98
        $files = glob("{$this->dir}/?*_?*.php");
99
        foreach ($files as $file) {
100
            preg_match('/^(?<sequence>[^_]+)_(?<class>.*)\.php$/', basename($file), $spec);
101
            $specs[$spec['sequence']] = [
102
                'file' => $file,
103
                'class' => "\\{$spec['class']}"
104
            ];
105
        }
106
        return $specs;
107
    }
108
109
    /**
110
     * Migrates up within a transaction.
111
     *
112
     * @param null|string $to Migration sequence identifier, or `null` for all upgrades.
113
     * @return false|string The resulting current sequence identifier.
114
     */
115
    public function up (string $to = null) {
116
        return $this->db->transact(function() use ($to) {
117
            $current = $this->getCurrent();
118
            // walk oldest to newest
119
            foreach ($this->glob() as $sequence => $spec) {
120
                if ($current and $to === $current) {
121
                    break;
122
                }
123
                if ($current >= $sequence) {
124
                    continue;
125
                }
126
                $this->db->transact(function() use ($sequence, $spec) {
127
                    $this->getMigration($spec)->up($this->db);
128
                    $this->table->insert(['sequence' => $sequence]);
129
                });
130
                $current = $sequence;
131
            }
132
            return $current;
133
        });
134
    }
135
}