Passed
Push — master ( 8e215e...9fb19b )
by y
01:44
created

Migrator::getTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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