Passed
Push — master ( 9cf790...f45573 )
by y
06:06
created

migrate.php$0 ➔ record_add_columns()   A

Complexity

Conditions 6
Paths 16

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 16
nop 3
dl 0
loc 22
rs 9.1111
c 0
b 0
f 0
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
        $up[] = "\$schema->createTable('{$record}', {$columns});";
221
        $down[] = "\$schema->dropTable('{$record}');";
222
223
        // add unique constraints
224
        foreach ($serializer->getUnique() as $column) {
225
            $up[] = "\$schema->addUniqueKeyConstraint('{$record}', ['{$column}']);";
226
            $down[] = "\$schema->dropUniqueKeyConstraint('{$record}', ['{$column}']);";
227
        }
228
        foreach ($serializer->getUniqueGroups() as $columns) {
229
            $columns = implode("', '", $columns);
230
            $up[] = "\$schema->addUniqueKeyConstraint('{$record}', ['{$columns}']);";
231
            $down[] = "\$schema->dropUniqueKeyConstraint('{$record}', ['{$columns}']);";
232
        }
233
    }
234
235
    private function record_create_eav(Record $record, &$up, &$down)
236
    {
237
        foreach ($record->getEav() as $eav) {
238
            if (!$this->schema->getTable($eav)) {
239
                $T_CONST = Schema::T_CONST_NAMES[$eav->getType()];
240
                $columns = [
241
                    "'entity' => Schema::T_INT | Schema::I_PRIMARY",
242
                    "'attribute' => Schema::T_STRING | Schema::I_PRIMARY",
243
                    "'value' => Schema::{$T_CONST}"
244
                ];
245
                $columns = "[\n\t\t\t" . implode(",\n\t\t\t", $columns) . "\n\t\t]";
246
                $foreign = "[\n\t\t\t'entity' => \$schema['{$record}']['id']\n\t\t]";
247
                $up[] = "\$schema->createTable('{$eav}', {$columns}, {$foreign});";
248
                $down[] = "\$schema->dropTable('{$eav}');";
249
            }
250
        }
251
    }
252
253
    private function record_add_columns(Record $record, &$up, &$down)
254
    {
255
        $columns = $this->schema->getColumnInfo($record);
256
        $serializer = $record->getSerializer();
257
        foreach ($serializer->getStorageTypes() as $property => $type) {
258
            if (!isset($columns[$property])) {
259
                $T_CONST = Schema::T_CONST_NAMES[$type];
260
                if ($serializer->isNullable($property)) {
261
                    $T_CONST .= '_NULL';
262
                }
263
                $up[] = "\$schema->addColumn('{$record}', '{$property}', Schema::{$T_CONST});";
264
                $down[] = "\$schema->dropColumn('{$record}', '{$property}');";
265
            }
266
        }
267
        foreach ($serializer->getUnique() as $property) {
268
            $up[] = "\$schema->addUniqueKeyConstraint('{$record}', ['{$property}']);";
269
            $down[] = "\$schema->dropUniqueKeyConstraint('{$record}', ['{$property}']);";
270
        }
271
        foreach ($serializer->getUniqueGroups() as $properties) {
272
            $properties = "'" . implode("','", $properties) . "'";
273
            $up[] = "\$schema->addUniqueKeyConstraint('{$record}', [{$properties}]);";
274
            $down[] = "\$schema->dropUniqueKeyConstraint('{$record}', [{$properties}]);";
275
        }
276
    }
277
278
    private function record_drop_columns(Record $record, &$up, &$down)
279
    {
280
        $columns = $this->schema->getColumnInfo($record);
281
        foreach ($columns as $column => $info) {
282
            if (!$record[$column]) {
283
                $T_CONST = Schema::T_CONST_NAMES[$info['type']];
284
                if ($info['nullable']) {
285
                    $T_CONST .= '_NULL';
286
                }
287
                $up[] = "\$schema->dropColumn('{$record}', '{$column}');";
288
                $down[] = "\$schema->addColumn('{$record}', '{$column}', Schema::{$T_CONST});";
289
            }
290
        }
291
    }
292
293
    private function junction(string $class): void
294
    {
295
        $class = $this->_toClass($class) or $this->_usage_exit();
296
        $junction = $this->db->getJunction($class);
297
        $up = [];
298
        $down = [];
299
300
        if (!$this->schema->getTable($junction)) {
301
            $records = $junction->getRecords();
302
            $columns = array_map(
303
                fn(string $column) => "'{$column}' => Schema::T_INT | Schema::I_PRIMARY",
304
                array_keys($records)
305
            );
306
            $columns = "[\n\t\t\t" . implode(",\n\t\t\t", $columns) . "\n\t\t]";
307
            $foreign = array_map(
308
                fn(string $column, Record $record) => "'{$column}' => \$schema['{$record}']['id']",
309
                array_keys($records),
310
                $records
311
            );
312
            $foreign = "[\n\t\t\t" . implode(",\n\t\t\t", $foreign) . "\n\t\t]";
313
            $up[] = "\$schema->createTable('{$junction}', {$columns}, {$foreign});";
314
            $down[] = "\$schema->dropTable('{$junction}');";
315
        }
316
317
        $this->write($class, $up, $down);
318
    }
319
320
    private function write(string $class, array $up, array $down): void
321
    {
322
        if (!$up or !$down) {
323
            $this->_stdout("-- Nothing to do.");
324
            return;
325
        }
326
327
        // import stuff
328
        $use = array_map(fn($import) => "use {$import};", [
329
            MigrationInterface::class,
330
            Schema::class
331
        ]);
332
333
        // reverse the $down operations
334
        $down = array_reverse($down);
335
336
        // write
337
        $dir = $this->db->getMigrator()->getDir();
338
        is_dir($dir) or mkdir($dir, 0775, true);
339
        $date = DateTime::createFromFormat('U.u', microtime(true))->format('Y-m-d\TH:i:s.v\Z');
340
        $sequence = "{$date}_" . str_replace('\\', '_', $class);
341
        $file = "{$dir}/{$sequence}.php";
342
        $use = implode("\n", $use);
343
        $up = str_replace("\t", '    ', "\t\t" . implode("\n\t\t", $up));
344
        $down = str_replace("\t", '    ', "\t\t" . implode("\n\t\t", $down));
345
        $this->_stderr("-- Writing {$file}");
346
        $fh = fopen($file, 'w');
347
        fputs($fh, <<<MIGRATION
348
        <?php
349
350
        {$use}
351
352
        /** {$sequence} */
353
        return new class implements MigrationInterface {
354
355
            /**
356
             * @param Schema \$schema
357
             */
358
            public function up(\$schema)
359
            {
360
        {$up}
361
            }
362
363
            /**
364
             * @param Schema \$schema
365
             */
366
            public function down(\$schema)
367
            {
368
        {$down}
369
            }
370
371
        };
372
373
        MIGRATION
374
        );
375
    }
376
377
})->_exec();
378