Passed
Push — master ( 49b932...79af1c )
by y
02:30
created

migrate.php$0 ➔ record_create()   B

Complexity

Conditions 7
Paths 48

Size

Total Lines 37
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 25
c 0
b 0
f 0
nc 48
nop 3
dl 0
loc 37
rs 8.5866
1
#!/usr/bin/php
2
<?php
3
4
namespace Helix;
5
6
include_once __DIR__ . "/.init.php";
7
8
use DateTime;
9
use Helix\DB\MigrationInterface;
10
use Helix\DB\Record;
11
use Helix\DB\Schema;
12
13
$opt = getopt('h', [
14
    'config:',
15
    'connection:',
16
    'help',
17
    'status',
18
    'up::',
19
    'down::',
20
    'record:',
21
    'junction:',
22
]);
23
24
(new class ($argv, $opt) {
25
26
    private array $argv;
27
28
    private array $opt;
29
30
    private DB $db;
31
32
    private Schema $schema;
33
34
    public function __construct(array $argv, array $opt)
35
    {
36
        $this->argv = $argv;
37
        $opt['connection'] ??= 'default';
38
        $opt['config'] ??= 'db.config.php';
39
        $this->opt = $opt;
40
        $this->db = DB::fromConfig($opt['connection'], $opt['config']);
41
        $realLogger = $this->db->getLogger();
42
        $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

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