Passed
Push — master ( 5e566a...b3ece8 )
by y
01:34
created

migrate.php$0 ➔ up()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 16
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 4
nop 1
dl 0
loc 16
rs 9.5222
c 0
b 0
f 0
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
    private Schema $schema;
30
31
    public function __construct(array $argv, array $opt)
32
    {
33
        $this->argv = $argv;
34
        $opt['connection'] ??= 'default';
35
        $opt['config'] ??= 'db.config.php';
36
        $this->opt = $opt;
37
        $this->db = DB::fromConfig($opt['connection'], $opt['config']);
38
        $realLogger = $this->db->getLogger();
39
        $this->db->setLogger(fn($sql) => $this->_stdout($sql) or $realLogger($sql));
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } or $realLogger($sql) of type boolean is incompatible with the type callable 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

39
        $this->db->setLogger(/** @scrutinizer ignore-type */ fn($sql) => $this->_stdout($sql) or $realLogger($sql));
Loading history...
Comprehensibility Best Practice introduced by
The variable $sql seems to be never defined.
Loading history...
40
        $this->schema = $this->db->getSchema();
41
    }
42
43
    private function _stderr(string $text): void
44
    {
45
        fputs(STDERR, "{$text}\n\n");
46
    }
47
48
    private function _stdout(string $text): void
49
    {
50
        echo "{$text}\n\n";
51
    }
52
53
    private function _usage_exit(): void
54
    {
55
        $this->_stderr(<<< USAGE
56
57
        $ php {$this->argv[0]} [OPTIONS] ACTION
58
59
        OPTIONS:
60
61
            --config=db.config.php
62
63
                Chooses the configuration file.
64
65
            --connection=default
66
67
                Chooses the connection from the configuration file.
68
69
        ACTIONS:
70
71
            -h
72
            --help
73
74
                Prints this usage information to STDERR and calls exit(1)
75
76
            --status
77
78
                Outputs the current migration sequence.
79
80
            --up=
81
            --down=
82
83
                Migrates up or down, optionally to a target sequence.
84
                For upgrades, the default target is all the way.
85
                For downgrades, the default target is the previous sequence.
86
87
            --record=<CLASS>
88
            --junction=<INTERFACE>
89
90
                The FQN of an annotated class or interface,
91
                which the DB instance can use to return Record or Junction access.
92
93
                The access object's tables are inspected against the database,
94
                and appropriate migration is then generated into the migrations
95
                directory. Statically generated migrations preserve history.
96
97
                To make CLI execution easier, forward-slashes in the FQN are
98
                converted to namespace separators.
99
        USAGE
100
        );
101
        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...
102
    }
103
104
    /**
105
     * @uses h()
106
     * @uses help()
107
     * @uses status()
108
     * @uses up()
109
     * @uses down()
110
     * @uses record()
111
     * @uses junction()
112
     */
113
    public function _exec(): void
114
    {
115
        foreach (['h', 'help', 'status', 'up', 'down', 'record', 'junction'] as $action) {
116
            if (isset($this->opt[$action])) {
117
                $this->{$action}($this->opt[$action] ?: null);
118
                return;
119
            }
120
        }
121
        $this->_usage_exit();
122
    }
123
124
    private function h(): void
125
    {
126
        $this->_usage_exit();
127
    }
128
129
    private function help(): void
130
    {
131
        $this->_usage_exit();
132
    }
133
134
    private function status(): void
135
    {
136
        $migrator = $this->db->getMigrator();
137
        $transaction = $this->db->newTransaction();
138
        $current = $migrator->getCurrent() ?? 'NONE';
139
        $this->_stdout("-- Current Migration State: {$current}");
140
        unset($transaction);
141
    }
142
143
    private function up(?string $to): void
144
    {
145
        $migrator = $this->db->getMigrator();
146
        $transaction = $this->db->newTransaction();
147
        $current = $migrator->getCurrent();
148
        $currentString = $current ?: 'NONE';
149
        if ($to) {
150
            $this->_stdout("-- Upgrading from \"{$currentString}\" to \"{$to}\" ...");
151
        } else {
152
            $this->_stdout("-- Upgrading ALL starting from \"{$currentString}\" ...");
153
        }
154
        sleep(3); // time to cancel
155
        if ($current === $migrator->up($to ?: null)) {
156
            $this->_stdout("-- Nothing to do.");
157
        } else {
158
            $transaction->commit();
159
        }
160
    }
161
162
    private function down(?string $to): void
163
    {
164
        $migrator = $this->db->getMigrator();
165
        $transaction = $this->db->newTransaction();
166
        $current = $migrator->getCurrent();
167
        $currentString = $current ?: 'NONE';
168
        if ($to) {
169
            $this->_stdout("-- Downgrading from \"{$currentString}\" to \"{$to}\" ...");
170
        } else {
171
            $this->_stdout("-- Downgrading once from \"{$currentString}\" ...");
172
        }
173
        sleep(3); // time to cancel
174
        if ($current === $migrator->down($to ?: null)) {
175
            $this->_stdout("-- Nothing to do.");
176
        } else {
177
            $transaction->commit();
178
        }
179
    }
180
181
    private function _toClass(string $path): string
182
    {
183
        return str_replace('/', '\\', $path);
184
    }
185
186
    private function record(string $class): void
187
    {
188
        $class = $this->_toClass($class) or $this->_usage_exit();
189
        $record = $this->db->getRecord($class);
190
        $up = [];
191
        $down = [];
192
        if (!$this->schema->getTable($record)) {
193
            $this->record_create($record, $up, $down);
194
        } else {
195
            $this->record_add_columns($record, $up, $down);
196
            $this->record_drop_columns($record, $up, $down);
197
        }
198
        $this->record_create_eav($record, $up, $down);
199
        $this->write($class, $up, $down);
200
    }
201
202
    private function record_create_eav(Record $record, &$up, &$down)
203
    {
204
        foreach ($record->getEav() as $eav) {
205
            if (!$this->schema->getTable($eav)) {
206
                $T_CONST = Schema::T_CONST_NAMES[$eav->getType()];
207
                $columns = [
208
                    "'entity' => Schema::T_INT | Schema::I_PRIMARY",
209
                    "'attribute' => Schema::T_STRING | Schema::I_PRIMARY",
210
                    "'value' => Schema::{$T_CONST}"
211
                ];
212
                $columns = "[\n\t\t\t" . implode(",\n\t\t\t", $columns) . "\n\t\t]";
213
                $foreign = "[\n\t\t\t'entity' => \$schema['{$record}']['id']\n\t\t]";
214
                $up[] = "\$schema->createTable('{$eav}', {$columns}, {$foreign});";
215
                $down[] = "\$schema->dropTable('{$eav}');";
216
            }
217
        }
218
    }
219
220
    private function record_add_columns(Record $record, &$up, &$down)
221
    {
222
        $columns = $this->schema->getColumnInfo($record);
223
        $multiUnique = [];
224
        foreach ($record->getTypes() as $property => $type) {
225
            if (!isset($columns[$property])) {
226
                $T_CONST = Schema::T_CONST_NAMES[$type];
227
                if ($record->isNullable($property)) {
228
                    $T_CONST .= '_NULL';
229
                }
230
                $up[] = "\$schema->addColumn('{$record}', '{$property}', Schema::{$T_CONST});";
231
                $down[] = "\$schema->dropColumn('{$record}', '{$property}');";
232
            }
233
            if ($record->isUnique($property)) {
234
                $up[] = "\$schema->addUniqueKeyConstraint('{$record}', ['{$property}']);";
235
                $down[] = "\$schema->dropUniqueKeyConstraint('{$record}', ['{$property}']);";
236
            } elseif ($uniqueGroup = $record->getUniqueGroup($property)) {
237
                $multiUnique[$uniqueGroup][] = $property;
238
            }
239
        }
240
        foreach ($multiUnique as $properties) {
241
            $properties = "'" . implode("','", $properties) . "'";
242
            $up[] = "\$schema->addUniqueKeyConstraint('{$record}', [{$properties}]);";
243
            $down[] = "\$schema->dropUniqueKeyConstraint('{$record}', [{$properties}]);";
244
        }
245
    }
246
247
    private function record_drop_columns(Record $record, &$up, &$down)
248
    {
249
        $columns = $this->schema->getColumnInfo($record);
250
        foreach ($columns as $column => $info) {
251
            if (!$record[$column]) {
252
                $T_CONST = Schema::T_CONST_NAMES[$info['type']];
253
                if ($info['nullable']) {
254
                    $T_CONST .= '_NULL';
255
                }
256
                $up[] = "\$schema->dropColumn('{$record}', '{$column}');";
257
                $down[] = "\$schema->addColumn('{$record}', '{$column}', Schema::{$T_CONST});";
258
            }
259
        }
260
    }
261
262
    private function record_create(Record $record, &$up, &$down)
263
    {
264
        $columns = [];
265
        foreach ($record->getTypes() as $property => $type) {
266
            $T_CONST = Schema::T_CONST_NAMES[$type];
267
            if ($record->isNullable($property)) {
268
                $T_CONST .= '_NULL';
269
            }
270
            $columns[$property] = "'{$property}' => Schema::{$T_CONST}";
271
        }
272
273
        $columns['id'] = "'id' => Schema::T_AUTOINCREMENT";
274
        $columns = "[\n\t\t\t" . implode(",\n\t\t\t", $columns) . "\n\t\t]";
275
        $up[] = "\$schema->createTable('{$record}', {$columns});";
276
        $down[] = "\$schema->dropTable('{$record}');";
277
278
        foreach ($record->getUnique() as $unique) {
279
            if (is_array($unique)) {
280
                $unique = implode("', '", $unique);
281
            }
282
            $up[] = "\$schema->addUniqueKeyConstraint('{$record}', ['{$unique}']);";
283
            $down[] = "\$schema->dropUniqueKeyConstraint('{$record}', ['{$unique}']);";
284
        }
285
    }
286
287
    private function junction(string $class): void
288
    {
289
        $class = $this->_toClass($class) or $this->_usage_exit();
290
        $junction = $this->db->getJunction($class);
291
        $up = [];
292
        $down = [];
293
294
        if (!$this->schema->getTable($junction)) {
295
            $records = $junction->getRecords();
296
            $columns = array_map(
297
                fn(string $column) => "'{$column}' => Schema::T_INT | Schema::I_PRIMARY",
298
                array_keys($records)
299
            );
300
            $columns = "[\n\t\t\t" . implode(",\n\t\t\t", $columns) . "\n\t\t]";
301
            $foreign = array_map(
302
                fn(string $column, Record $record) => "'{$column}' => \$schema['{$record}']['id']",
303
                array_keys($records),
304
                $records
305
            );
306
            $foreign = "[\n\t\t\t" . implode(",\n\t\t\t", $foreign) . "\n\t\t]";
307
            $up[] = "\$schema->createTable('{$junction}', {$columns}, {$foreign});";
308
            $down[] = "\$schema->dropTable('{$junction}');";
309
        }
310
311
        $this->write($class, $up, $down);
312
    }
313
314
    private function write(string $class, array $up, array $down): void
315
    {
316
        if (!$up or !$down) {
317
            $this->_stdout("-- Nothing to do.");
318
            return;
319
        }
320
321
        // import stuff
322
        $use = array_map(fn($import) => "use {$import};", [
323
            MigrationInterface::class,
324
            Schema::class
325
        ]);
326
327
        // reverse the $down operations
328
        $down = array_reverse($down);
329
330
        // write
331
        $dir = $this->db->getMigrator()->getDir();
332
        is_dir($dir) or mkdir($dir, 0775, true);
333
        $date = DateTime::createFromFormat('U.u', microtime(true))->format('Y-m-d\TH:i:s.v\Z');
334
        $sequence = "{$date}_" . str_replace('\\', '_', $class);
335
        $file = "{$dir}/{$sequence}.php";
336
        $use = implode("\n", $use);
337
        $up = str_replace("\t", '    ', "\t\t" . implode("\n\t\t", $up));
338
        $down = str_replace("\t", '    ', "\t\t" . implode("\n\t\t", $down));
339
        $this->_stderr("-- Writing {$file}");
340
        $fh = fopen($file, 'w');
341
        fputs($fh, <<<MIGRATION
342
        <?php
343
344
        {$use}
345
346
        /** {$sequence} */
347
        return new class implements MigrationInterface {
348
349
            /**
350
             * @param Schema \$schema
351
             */
352
            public function up(\$schema)
353
            {
354
        {$up}
355
            }
356
357
            /**
358
             * @param Schema \$schema
359
             */
360
            public function down(\$schema)
361
            {
362
        {$down}
363
            }
364
365
        };
366
367
        MIGRATION
368
        );
369
    }
370
371
})->_exec();
372