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

helix.db.migrate.php$0 ➔ _stderr()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 2
rs 10
cc 1
nc 1
nop 1
1
#!/usr/bin/php
2
<?php
3
include_once __DIR__ . "/.init.php";
4
5
use Helix\DB;
6
use Helix\DB\MigrationInterface;
7
use Helix\DB\Record;
8
use Helix\DB\Schema;
9
10
$opt = getopt('h', [
11
    'config:',
12
    'connection:',
13
    'help',
14
    'status',
15
    'up::',
16
    'down::',
17
    'record:',
18
    'junction:',
19
]);
20
21
(new class ($argv, $opt) {
22
23
    private array $argv;
24
25
    private array $opt;
26
27
    private DB $db;
28
29
    public function __construct (array $argv, array $opt) {
30
        $this->argv = $argv;
31
        $opt['connection'] ??= 'default';
32
        $opt['config'] ??= 'db.config.php';
33
        $this->opt = $opt;
34
        $this->db = DB::fromConfig($opt['connection'], $opt['config']);
35
        $realLogger = $this->db->getLogger();
36
        $this->db->setLogger(fn($sql) => $this->_stdout($sql) and $realLogger($sql));
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } and $realLogger($sql) of type boolean is incompatible with the type Closure expected by parameter $logger of Helix\DB::setLogger(). ( Ignorable by Annotation )

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

36
        $this->db->setLogger(/** @scrutinizer ignore-type */ fn($sql) => $this->_stdout($sql) and $realLogger($sql));
Loading history...
Comprehensibility Best Practice introduced by
The variable $sql seems to be never defined.
Loading history...
37
    }
38
39
    private function _stderr (string $text): void {
40
        fputs(STDERR, "{$text}\n\n");
41
    }
42
43
    private function _stdout (string $text): void {
44
        echo "{$text}\n\n";
45
    }
46
47
    private function _usage_exit (): void {
48
        $this->_stderr(<<< USAGE
49
        
50
        $ php {$this->argv[0]} [OPTIONS] ACTION
51
52
        OPTIONS:
53
54
            --config=db.config.php
55
56
                Chooses the configuration file.
57
58
            --connection=default
59
60
                Chooses the connection from the configuration file.
61
62
        ACTIONS:
63
64
            -h
65
            --help
66
67
                Prints this usage information to STDERR and calls exit(1)
68
69
            --status
70
71
                Outputs the current migration sequence.
72
73
            --up=
74
            --down=
75
76
                Migrates up or down, optionally to a target sequence.
77
                For upgrades, the default target is all the way.
78
                For downgrades, the default target is the previous sequence.
79
80
            --record=<CLASS>
81
            --junction=<INTERFACE>
82
83
                The FQN of an annotated class or interface,
84
                which the DB instance can use to return Record or Junction access.
85
86
                The access object's tables are inspected against the database,
87
                and appropriate migration is then generated into the migrations
88
                directory. Statically generated migrations preserve history.
89
90
                To make CLI execution easier, forward-slashes in the FQN are
91
                converted to namespace separators.
92
        USAGE
93
        );
94
        exit(1);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
95
    }
96
97
    /**
98
     * @uses h()
99
     * @uses help()
100
     * @uses status()
101
     * @uses up()
102
     * @uses down()
103
     * @uses record()
104
     * @uses junction()
105
     */
106
    public function _exec (): void {
107
        foreach (['h', 'help', 'status', 'up', 'down', 'record', 'junction'] as $action) {
108
            if (isset($this->opt[$action])) {
109
                $this->{$action}($this->opt[$action] ?: null);
110
                return;
111
            }
112
        }
113
        $this->_usage_exit();
114
    }
115
116
    private function h (): void {
117
        $this->_usage_exit();
118
    }
119
120
    private function help (): void {
121
        $this->_usage_exit();
122
    }
123
124
    private function status (): void {
125
        $migrator = $this->db->getMigrator();
126
        $transaction = $this->db->newTransaction();
127
        $current = $migrator->getCurrent() ?? 'NONE';
128
        $this->_stdout("-- Current Migration State: {$current}");
129
        unset($transaction);
130
    }
131
132
    private function up (?string $to): void {
133
        $migrator = $this->db->getMigrator();
134
        $transaction = $this->db->newTransaction();
135
        $current = $migrator->getCurrent();
136
        $currentString = $current ?: 'NONE';
137
        if ($to) {
138
            $this->_stdout("-- Upgrading from \"{$currentString}\" to \"{$to}\" ...");
139
        }
140
        else {
141
            $this->_stdout("-- Upgrading ALL starting from \"{$currentString}\" ...");
142
        }
143
        sleep(3); // time to cancel
144
        if ($current === $migrator->up($to ?: null)) {
145
            $this->_stdout("-- Nothing to do.");
146
        }
147
        else {
148
            $transaction->commit();
149
        }
150
    }
151
152
    private function down (?string $to): void {
153
        $migrator = $this->db->getMigrator();
154
        $transaction = $this->db->newTransaction();
155
        $current = $migrator->getCurrent();
156
        $currentString = $current ?: 'NONE';
157
        if ($to) {
158
            $this->_stdout("-- Downgrading from \"{$currentString}\" to \"{$to}\" ...");
159
        }
160
        else {
161
            $this->_stdout("-- Downgrading once from \"{$currentString}\" ...");
162
        }
163
        sleep(3); // time to cancel
164
        if ($current === $migrator->down($to ?: null)) {
165
            $this->_stdout("-- Nothing to do.");
166
        }
167
        else {
168
            $transaction->commit();
169
        }
170
    }
171
172
    private function _toClass (string $path): string {
173
        return str_replace('/', '\\', $path);
174
    }
175
176
    private function record (string $class): void {
177
        $class = $this->_toClass($class) or $this->_usage_exit();
178
        $record = $this->db->getRecord($class);
179
        $use = [];
180
        $up = [];
181
        $down = [];
182
183
        // create table
184
        if (!$this->db[$record->getName()]) {
185
            $columns = [];
186
            foreach ($record->getTypes() as $property => $type) {
187
                $T_CONST = Schema::PHP_TYPE_NAMES[$type];
188
                $columns[$property] = "'{$property}' => Schema::{$T_CONST}";
189
                if (!$record->isNullable($property)) {
190
                    $columns[$property] .= '_STRICT';
191
                }
192
            }
193
            $columns['id'] = "'id' => Schema::T_AUTOINCREMENT";
194
            $columns = "[\n\t\t\t" . implode(",\n\t\t\t", $columns) . "\n\t\t]";
195
            $up[] = "\$schema->createTable('{$record}', {$columns});";
196
            $down[] = "\$schema->dropTable('{$record}');";
197
        }
198
199
        // check each eav
200
        foreach ($record->getEav() as $eav) {
201
            // create table
202
            if (!$this->db[$eav->getName()]) {
203
                $T_CONST = Schema::PHP_TYPE_NAMES[$eav->getType()];
204
                $columns = [
205
                    "'entity' => Schema::T_INT_STRICT",
206
                    "'attribute' => Schema::T_STRING_STRICT",
207
                    "'value' => Schema::{$T_CONST}"
208
                ];
209
                $columns = "[\n\t\t\t" . implode(",\n\t\t\t", $columns) . "\n\t\t]";
210
                $constraints = [
211
                    "Schema::TABLE_PRIMARY => ['entity', 'attribute']",
212
                    "Schema::TABLE_FOREIGN => \$schema['{$record}']['id']"
213
                ];
214
                $constraints = "[\n\t\t\t" . implode(",\n\t\t\t", $constraints) . "\n\t\t]";
215
                $up[] = "\$schema->createTable('{$eav}', {$columns}, {$constraints});";
216
                $down[] = "\$schema->dropTable('{$eav}');";
217
            }
218
        }
219
220
        $this->write($class, $use, $up, $down);
221
    }
222
223
    private function junction (string $class): void {
224
        $class = $this->_toClass($class) or $this->_usage_exit();
225
        $junction = $this->db->getJunction($class);
226
        $use = [];
227
        $up = [];
228
        $down = [];
229
230
        // create table
231
        if (!$this->db[$junction->getName()]) {
232
            $records = $junction->getRecords();
233
            $columns = array_map(
234
                fn(string $column) => "'{$column}' => Schema::T_INT_STRICT",
235
                array_keys($records)
236
            );
237
            $columns = "[\n\t\t\t" . implode(",\n\t\t\t", $columns) . "\n\t\t]";
238
            $primary = array_map(fn(string $column) => "'{$column}'", array_keys($records));
239
            $primary = "[" . implode(', ', $primary) . "]";
240
            $foreign = array_map(
241
                fn(string $column, Record $record) => "'{$column}' => \$schema['{$record}']['id']",
242
                array_keys($records),
243
                $records
244
            );
245
            $foreign = "[\n\t\t\t\t" . implode(",\n\t\t\t\t", $foreign) . "\n\t\t\t]";
246
            $constraints = [
247
                "Schema::TABLE_PRIMARY => {$primary}",
248
                "Schema::TABLE_FOREIGN => {$foreign}"
249
            ];
250
            $constraints = "[\n\t\t\t" . implode(",\n\t\t\t", $constraints) . "\n\t\t]";
251
            $up[] = "\$schema->createTable('{$junction}', {$columns}, {$constraints});";
252
            $down[] = "\$schema->dropTable('{$junction}');";
253
        }
254
255
        $this->write($class, $use, $up, $down);
256
    }
257
258
    private function write (string $class, array $use, array $up, array $down): void {
259
        if (!$up or !$down) {
260
            $this->_stdout("-- Nothing to do.");
261
            return;
262
        }
263
264
        // convert $use to imports
265
        $use[] = Schema::class;
266
        $use[] = MigrationInterface::class;
267
        sort($use);
268
        $use = array_map(fn(string $import) => "use {$import};", array_unique($use));
269
270
        // reverse the $down operations
271
        $down = array_reverse($down);
272
273
        // write
274
        $dir = $this->db->getMigrator()->getDir();
275
        is_dir($dir) or mkdir($dir, 0775, true);
276
        $date = DateTime::createFromFormat('U.u', microtime(true))->format('Y-m-d\TH:i:s.v\Z');
277
        $sequence = "{$date}_" . str_replace('\\', '_', $class);
278
        $file = "{$dir}/{$sequence}.php";
279
        $use = implode("\n", $use);
280
        $up = str_replace("\t", '    ', "\t\t" . implode("\n\t\t", $up));
281
        $down = str_replace("\t", '    ', "\t\t" . implode("\n\t\t", $down));
282
        $this->_stderr("-- Writing {$file}");
283
        $fh = fopen($file, 'w');
284
        fputs($fh, <<<MIGRATION
285
        <?php
286
        {$use}
287
        
288
        /** {$sequence} */
289
        return new class implements MigrationInterface {
290
291
            /**
292
             * @var Schema \$schema
293
             */
294
            public function up (\$schema)
295
            {
296
        {$up}
297
            }
298
299
            /**
300
             * @var Schema \$schema
301
             */
302
            public function down (\$schema)
303
            {
304
        {$down}
305
            }
306
307
        };
308
309
        MIGRATION
310
        );
311
    }
312
313
})->_exec();
314