Passed
Push — master ( f45573...49b932 )
by y
02:08 queued 12s
created

anonymous//bin/migrate.php$0   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 346
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 166
dl 0
loc 346
rs 6.96
c 0
b 0
f 0
wmc 53

18 Methods

Rating   Name   Duplication   Size   Complexity  
A migrate.php$0 ➔ write() 0 49 4
A migrate.php$0 ➔ junction() 0 25 3
A migrate.php$0 ➔ status() 0 7 1
A migrate.php$0 ➔ record_create_eav() 0 14 3
A migrate.php$0 ➔ record_create() 0 27 5
A migrate.php$0 ➔ down() 0 16 5
A migrate.php$0 ➔ h() 0 3 1
A migrate.php$0 ➔ help() 0 3 1
A migrate.php$0 ➔ __construct() 0 10 2
A migrate.php$0 ➔ record() 0 14 3
A migrate.php$0 ➔ up() 0 16 5
A migrate.php$0 ➔ _stderr() 0 3 1
A migrate.php$0 ➔ _usage_exit() 0 49 1
A migrate.php$0 ➔ _toClass() 0 3 1
A migrate.php$0 ➔ _exec() 0 9 4
A migrate.php$0 ➔ _stdout() 0 3 1
A migrate.php$0 ➔ record_drop_columns() 0 8 4
B migrate.php$0 ➔ record_add_columns() 0 24 8

How to fix   Complexity   

Complex Class

Complex classes like anonymous//bin/migrate.php$0 often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use anonymous//bin/migrate.php$0, and based on these observations, apply Extract Interface, too.

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->addUniqueKey('{$record}', ['{$column}']);";
226
            $down[] = "\$schema->dropUniqueKey('{$record}', ['{$column}']);";
227
        }
228
        foreach ($serializer->getUniqueGroups() as $columns) {
229
            $columns = implode("', '", $columns);
230
            $up[] = "\$schema->addUniqueKey('{$record}', ['{$columns}']);";
231
            $down[] = "\$schema->dropUniqueKey('{$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
            }
265
        }
266
        foreach ($serializer->getUnique() as $property) {
267
            if (!$this->schema->hasUniqueKey($record, [$property])) {
268
                $up[] = "\$schema->addUniqueKey('{$record}', ['{$property}']);";
269
                $down[] = "\$schema->dropUniqueKey('{$record}', ['{$property}']);";
270
            }
271
        }
272
        foreach ($serializer->getUniqueGroups() as $properties) {
273
            if (!$this->schema->hasUniqueKey($record, $properties)) {
274
                $properties = "'" . implode("','", $properties) . "'";
275
                $up[] = "\$schema->addUniqueKey('{$record}', [{$properties}]);";
276
                $down[] = "\$schema->dropUniqueKey('{$record}', [{$properties}]);";
277
            }
278
        }
279
    }
280
281
    private function record_drop_columns(Record $record, &$up, &$down)
282
    {
283
        $columns = $this->schema->getColumnInfo($record);
284
        foreach ($columns as $column => $info) {
285
            if (!$record[$column]) {
286
                if ($this->schema->hasUniqueKey($record, [$column])) {
287
                    $up[] = "\$schema->dropUniqueKey('{$record}', ['{$column}']);";
288
                    $down[] = "\$schema->addUniqueKey('{$record}', ['{$column}']);";
289
                }
290
            }
291
        }
292
    }
293
294
    private function junction(string $class): void
295
    {
296
        $class = $this->_toClass($class) or $this->_usage_exit();
297
        $junction = $this->db->getJunction($class);
298
        $up = [];
299
        $down = [];
300
301
        if (!$this->schema->getTable($junction)) {
302
            $records = $junction->getRecords();
303
            $columns = array_map(
304
                fn(string $column) => "'{$column}' => Schema::T_INT | Schema::I_PRIMARY",
305
                array_keys($records)
306
            );
307
            $columns = "[\n\t\t\t" . implode(",\n\t\t\t", $columns) . "\n\t\t]";
308
            $foreign = array_map(
309
                fn(string $column, Record $record) => "'{$column}' => \$schema['{$record}']['id']",
310
                array_keys($records),
311
                $records
312
            );
313
            $foreign = "[\n\t\t\t" . implode(",\n\t\t\t", $foreign) . "\n\t\t]";
314
            $up[] = "\$schema->createTable('{$junction}', {$columns}, {$foreign});";
315
            $down[] = "\$schema->dropTable('{$junction}');";
316
        }
317
318
        $this->write($class, $up, $down);
319
    }
320
321
    private function write(string $class, array $up, array $down): void
322
    {
323
        if (!$up or !$down) {
324
            $this->_stdout("-- Nothing to do.");
325
            return;
326
        }
327
328
        // import stuff
329
        $use = array_map(fn($import) => "use {$import};", [
330
            MigrationInterface::class,
331
            Schema::class
332
        ]);
333
334
        // reverse the $down operations
335
        $down = array_reverse($down);
336
337
        // write
338
        $dir = $this->db->getMigrator()->getDir();
339
        is_dir($dir) or mkdir($dir, 0775, true);
340
        $date = DateTime::createFromFormat('U.u', microtime(true))->format('Y-m-d\TH:i:s.v\Z');
341
        $sequence = "{$date}_" . str_replace('\\', '_', $class);
342
        $file = "{$dir}/{$sequence}.php";
343
        $use = implode("\n", $use);
344
        $up = str_replace("\t", '    ', "\t\t" . implode("\n\t\t", $up));
345
        $down = str_replace("\t", '    ', "\t\t" . implode("\n\t\t", $down));
346
        $this->_stderr("-- Writing {$file}");
347
        $fh = fopen($file, 'w');
348
        fputs($fh, <<<MIGRATION
349
        <?php
350
351
        {$use}
352
353
        /** {$sequence} */
354
        return new class implements MigrationInterface {
355
356
            /**
357
             * @param Schema \$schema
358
             */
359
            public function up(\$schema)
360
            {
361
        {$up}
362
            }
363
364
            /**
365
             * @param Schema \$schema
366
             */
367
            public function down(\$schema)
368
            {
369
        {$down}
370
            }
371
372
        };
373
374
        MIGRATION
375
        );
376
    }
377
378
})->_exec();
379