Migrator::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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