TableFacade::setValueAttrs()   A
last analyzed

Complexity

Conditions 5
Paths 7

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 6
nc 7
nop 1
dl 0
loc 12
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
namespace Lagdo\DbAdmin\Db\Driver\Facades;
4
5
use Lagdo\DbAdmin\Driver\Entity\ForeignKeyEntity;
6
use Lagdo\DbAdmin\Driver\Entity\TableEntity;
7
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
8
use Exception;
9
10
use function array_key_exists;
11
use function array_map;
12
use function array_merge;
13
use function compact;
14
use function implode;
15
use function intval;
16
use function ksort;
17
use function preg_match;
18
use function str_replace;
19
use function trim;
20
21
/**
22
 * Facade to table functions
23
 */
24
class TableFacade extends AbstractFacade
25
{
26
    /**
27
     * The current table status
28
     *
29
     * @var mixed
30
     */
31
    protected $tableStatus = null;
32
33
    /**
34
     * @var array
35
     */
36
    protected $referencableTables = [];
37
38
    /**
39
     * @var array
40
     */
41
    protected $foreignKeys = [];
42
43
    /**
44
     * @var string
45
     */
46
    private $after = '';
47
48
    /**
49
     * @var array
50
     */
51
    private $fields = [];
52
53
    /**
54
     * @var TableEntity
55
     */
56
    private $attrs;
57
58
    /**
59
     * Get foreign keys
60
     *
61
     * @param string $table     The table name
62
     *
63
     * @return void
64
     */
65
    private function getForeignKeys(string $table = '')
66
    {
67
        $this->referencableTables = $this->driver->referencableTables($table);
0 ignored issues
show
Bug introduced by
The method referencableTables() does not exist on null. ( Ignorable by Annotation )

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

67
        /** @scrutinizer ignore-call */ 
68
        $this->referencableTables = $this->driver->referencableTables($table);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
68
        $this->foreignKeys = [];
69
        foreach ($this->referencableTables as $tableName => $field) {
70
            $name = str_replace('`', '``', $tableName) . '`' .
71
                str_replace('`', '``', $field->name);
72
            // not escapeId() - used in JS
73
            $this->foreignKeys[$name] = $tableName;
74
        }
75
    }
76
77
    /**
78
     * Get field types
79
     *
80
     * @param string $type  The type name
81
     *
82
     * @return array
83
     */
84
    public function getFieldTypes(string $type = ''): array
85
    {
86
        // From includes/editing.inc.php
87
        $extraTypes = [];
88
        if ($type && !$this->driver->typeExists($type) && !isset($this->foreignKeys[$type]) &&
89
            !array_key_exists($this->utils->trans->lang('Current'), $extraTypes)) {
90
            $extraTypes[$this->utils->trans->lang('Current')] = [$type];
91
        }
92
        if (!empty($this->foreignKeys)) {
93
            $this->driver->setStructuredType($this->utils->trans->lang('Foreign keys'), $this->foreignKeys);
94
        }
95
        return array_merge($extraTypes, $this->driver->structuredTypes());
96
    }
97
98
    /**
99
     * Get the current table status
100
     *
101
     * @param string $table
102
     *
103
     * @return mixed
104
     */
105
    protected function status(string $table)
106
    {
107
        if (!$this->tableStatus) {
108
            $this->tableStatus = $this->driver->tableStatusOrName($table, true);
109
        }
110
        return $this->tableStatus;
111
    }
112
113
    /**
114
     * @param TableFieldEntity $field
115
     * @param string $orig
116
     * @param string $table
117
     *
118
     * @return void
119
     */
120
    private function addFieldToAttrs(TableFieldEntity $field, string $orig, string $table)
121
    {
122
        if ($field->name === '' && $orig !== '') {
123
            // A missing "name" field and a not empty "orig" field means the column is to be dropped.
124
            $this->attrs->dropped[] = $orig;
125
            return;
126
        }
127
        $foreignKey = $this->foreignKeys[$field->type] ?? null;
128
        //! Can collide with user defined type
129
        $typeField = ($foreignKey === null ? $field :
130
            TableFieldEntity::make($this->referencableTables[$foreignKey]));
131
        $processedField = $this->driver->processField($field, $typeField);
132
        $origField = $this->fields[$field->name] ?? null;
133
        $this->after = '';
134
        if ($orig === '') {
135
            $this->attrs->fields[] = ['', $processedField, $this->after];
136
            $this->after = ' AFTER ' . $this->driver->escapeId($field->name);
137
        } elseif ($origField !== null && !$field->equals($origField)) {
138
            $this->attrs->edited[] = [$orig, $processedField, $this->after];
139
        }
140
        if ($foreignKey !== null) {
141
            $fkey = new ForeignKeyEntity();
142
            $fkey->table = $this->foreignKeys[$field->type];
143
            $fkey->source = [$field->name];
144
            $fkey->target = [$typeField->name];
145
            $fkey->onDelete = $field->onDelete;
146
            $this->attrs->foreign[$this->driver->escapeId($field->name)] =
147
                ($table != '' && $this->driver->jush() != 'sqlite' ? 'ADD' : ' ') .
148
                $this->driver->formatForeignKey($fkey);
149
        }
150
    }
151
152
    /**
153
     * @return void
154
     */
155
    // private function setPartitionAttr()
156
    // {
157
    //     $this->attrs->partitioning = '';
158
    //     if($partition_by[$values['partition_by']]) {
159
    //         $partitions = [];
160
    //         if($values['partition_by'] == 'RANGE' || $values['partition_by'] == 'LIST')
161
    //         {
162
    //             foreach(\array_filter($values['partition_names']) as $key => $val)
163
    //             {
164
    //                 $value = $values['partition_values'][$key];
165
    //                 // Todo: use match
166
    //                 $partitions[] = "\n  PARTITION " . $this->driver->escapeId($val) .
167
    //                     ' VALUES ' . ($values['partition_by'] == 'RANGE' ? 'LESS THAN' : 'IN') .
168
    //                     ($value != '' ? ' ($value)' : ' MAXVALUE'); //! SQL injection
169
    //             }
170
    //         }
171
    //         $this->attrs->partitioning .= "\nPARTITION BY $values[partition_by]($values[partition])" .
172
    //             ($partitions // $values['partition'] can be expression, not only column
173
    //             ? ' (' . \implode(',', $partitions) . "\n)"
174
    //             : ($values['partitions'] ? ' PARTITIONS ' . (+$values['partitions']) : '')
175
    //         );
176
    //     } elseif($this->driver->support('partitioning') &&
177
    //         \preg_match('~partitioned~', $this->tableStatus->Create_options)) {
178
    //         $this->attrs->partitioning .= "\nREMOVE PARTITIONING";
179
    //     }
180
    // }
181
182
    /**
183
     * @param array $values
184
     *
185
     * @return void
186
     */
187
    private function setValueAttrs(array $values)
188
    {
189
        foreach (['comment', 'engine', 'collation'] as $attr) {
190
            $this->attrs->$attr = !empty($values[$attr]) ? $values[$attr] : '';
191
            if ($this->tableStatus != null) {
192
                // No change.
193
                if ($this->attrs->$attr == $this->tableStatus->$attr) {
194
                    $this->attrs->$attr = '';
195
                }
196
            }
197
        }
198
        $this->attrs->autoIncrement = intval($this->utils->str->number($this->utils->input->getAutoIncrementStep()));
199
    }
200
201
    /**
202
     * @param TableEntity $status
203
     *
204
     * @return array<string, string>
205
     */
206
    private function getTabs(TableEntity $status): array
207
    {
208
        $tabs = [
209
            'fields' => $this->utils->trans->lang('Columns'),
210
            // 'indexes' => $this->utils->trans->lang('Indexes'),
211
            // 'foreign-keys' => $this->utils->trans->lang('Foreign keys'),
212
            // 'triggers' => $this->utils->trans->lang('Triggers'),
213
        ];
214
        if ($this->driver->isView($status)) {
215
            if ($this->driver->support('view_trigger')) {
216
                $tabs['triggers'] = $this->utils->trans->lang('Triggers');
217
            }
218
            return $tabs;
219
        }
220
221
        if ($this->driver->support('indexes')) {
222
            $tabs['indexes'] = $this->utils->trans->lang('Indexes');
223
        }
224
        if ($this->driver->supportForeignKeys($status)) {
225
            $tabs['foreign-keys'] = $this->utils->trans->lang('Foreign keys');
226
        }
227
        if ($this->driver->support('trigger')) {
228
            $tabs['triggers'] = $this->utils->trans->lang('Triggers');
229
        }
230
        return $tabs;
231
    }
232
233
    /**
234
     * Get details about a table
235
     *
236
     * @param string $table     The table name
237
     *
238
     * @return array
239
     */
240
    public function getTableInfo(string $table): array
241
    {
242
        // From table.inc.php
243
        $status = $this->status($table);
244
        $name = $this->page->tableName($status);
245
246
        return [
247
            'title' => $this->utils->trans->lang('Table') . ': ' .
248
                ($name != '' ? $name : $this->utils->str->html($table)),
249
            'comment' => $status->comment,
250
            'tabs' => $this->getTabs($status),
251
        ];
252
    }
253
254
    /**
255
     * Get the fields of a table
256
     *
257
     * @param string $table The table name
258
     *
259
     * @return array
260
     * @throws Exception
261
     */
262
    public function getTableFields(string $table): array
263
    {
264
        // From table.inc.php
265
        $fields = $this->driver->fields($table);
266
        if (empty($fields)) {
267
            throw new Exception($this->driver->error());
268
        }
269
270
        $headers = [
271
            $this->utils->trans->lang('Name'),
272
            $this->utils->trans->lang('Type'),
273
            $this->utils->trans->lang('Collation'),
274
        ];
275
        $hasComment = $this->driver->support('comment');
276
        if ($hasComment) {
277
            $headers[] = $this->utils->trans->lang('Comment');
278
        }
279
280
        $details = [];
281
        foreach ($fields as $field) {
282
            $detail = [
283
                'name' => $this->utils->str->html($field->name),
284
                'type' => $this->page->getTableFieldType($field),
285
                'collation' => $this->utils->str->html($field->collation),
286
            ];
287
            if ($hasComment) {
288
                $detail['comment'] = $this->utils->str->html($field->comment);
289
            }
290
            $details[] = $detail;
291
        }
292
293
        return compact('headers', 'details');
294
    }
295
296
    /**
297
     * Get the indexes of a table
298
     *
299
     * @param string $table     The table name
300
     *
301
     * @return array|null
302
     */
303
    public function getTableIndexes(string $table): ?array
304
    {
305
        if (!$this->driver->support('indexes')) {
306
            return null;
307
        }
308
309
        // From table.inc.php
310
        $indexes = $this->driver->indexes($table);
311
312
        $headers = [
313
            $this->utils->trans->lang('Name'),
314
            $this->utils->trans->lang('Type'),
315
            $this->utils->trans->lang('Column'),
316
        ];
317
318
        $details = [];
319
        // From adminer.inc.php
320
        foreach ($indexes as $name => $index) {
321
            ksort($index->columns); // enforce correct columns order
322
            $print = [];
323
            foreach ($index->columns as $key => $val) {
324
                $value = '<i>' . $this->utils->str->html($val) . '</i>';
325
                if (array_key_exists($key, $index->lengths)) {
326
                    $value .= '(' . $index->lengths[$key] . ')';
327
                }
328
                if (array_key_exists($key, $index->descs)) {
329
                    $value .= ' DESC';
330
                }
331
                $print[] = $value;
332
            }
333
            $details[] = [
334
                'name' => $this->utils->str->html($name),
335
                'type' => $index->type,
336
                'desc' => implode(', ', $print),
337
            ];
338
        }
339
340
        return compact('headers', 'details');
341
    }
342
343
    /**
344
     * Get the foreign keys of a table
345
     *
346
     * @param string $table     The table name
347
     *
348
     * @return array|null
349
     */
350
    public function getTableForeignKeys(string $table): ?array
351
    {
352
        $status = $this->status($table);
353
        if (!$this->driver->supportForeignKeys($status)) {
354
            return null;
355
        }
356
357
        $headers = [
358
            $this->utils->trans->lang('Name'),
359
            $this->utils->trans->lang('Source'),
360
            $this->utils->trans->lang('Target'),
361
            $this->utils->trans->lang('ON DELETE'),
362
            $this->utils->trans->lang('ON UPDATE'),
363
        ];
364
365
        $foreignKeys = $this->driver->foreignKeys($table);
366
        $details = [];
367
        // From table.inc.php
368
        foreach ($foreignKeys as $name => $foreignKey) {
369
            $target = '';
370
            if ($foreignKey->database != '') {
371
                $target .= '<b>' . $this->utils->str->html($foreignKey->database) . '</b>.';
372
            }
373
            if ($foreignKey->schema != '') {
374
                $target .= '<b>' . $this->utils->str->html($foreignKey->schema) . '</b>.';
375
            }
376
            $target = $this->utils->str->html($foreignKey->table) .
377
                '(' . implode(', ', array_map(function ($key) {
378
                    return $this->utils->str->html($key);
379
                }, $foreignKey->target)) . ')';
380
            $details[] = [
381
                'name' => $this->utils->str->html($name),
382
                'source' => '<i>' . implode(
383
                    '</i>, <i>',
384
                    array_map(function ($key) {
385
                        return $this->utils->str->html($key);
386
                    }, $foreignKey->source)
387
                ) . '</i>',
388
                'target' => $target,
389
                'onDelete' => $this->utils->str->html($foreignKey->onDelete),
390
                'onUpdate' => $this->utils->str->html($foreignKey->onUpdate),
391
            ];
392
        }
393
394
        return compact('headers', 'details');
395
    }
396
397
    /**
398
     * Get the triggers of a table
399
     *
400
     * @param string $table     The table name
401
     *
402
     * @return array|null
403
     */
404
    public function getTableTriggers(string $table): ?array
405
    {
406
        if (!$this->driver->support('trigger')) {
407
            return null;
408
        }
409
410
        $headers = [
411
            $this->utils->trans->lang('Name'),
412
            '&nbsp;',
413
            '&nbsp;',
414
            '&nbsp;',
415
        ];
416
417
        $details = [];
418
        // From table.inc.php
419
        $triggers = $this->driver->triggers($table);
420
        foreach ($triggers as $name => $trigger) {
421
            $details[] = [
422
                $this->utils->str->html($trigger->timing),
423
                $this->utils->str->html($trigger->event),
424
                $this->utils->str->html($name),
425
                $this->utils->trans->lang('Alter'),
426
            ];
427
        }
428
429
        return compact('headers', 'details');
430
    }
431
432
    /**
433
     * Get required data for create/update on tables
434
     *
435
     * @param string $table The table name
436
     *
437
     * @return array
438
     * @throws Exception
439
     */
440
    public function getTableData(string $table = ''): array
441
    {
442
        // From create.inc.php
443
        $status = [];
444
        $fields = [];
445
        if ($table !== '') {
446
            $status = $this->driver->tableStatus($table);
447
            if (!$status) {
448
                throw new Exception($this->utils->trans->lang('No tables.'));
449
            }
450
            $fields = $this->driver->fields($table);
451
        }
452
453
        $this->getForeignKeys($table);
454
455
        $hasAutoIncrement = false;
456
        foreach ($fields as &$field) {
457
            $hasAutoIncrement = $hasAutoIncrement || $field->autoIncrement;
458
            if (preg_match('~^CURRENT_TIMESTAMP~i', $field->onUpdate)) {
459
                $field->onUpdate = 'CURRENT_TIMESTAMP';
460
            }
461
462
            $type = $field->type;
463
            $field->types = $this->getFieldTypes($type);
464
            $field->lengthRequired = !$field->length && preg_match('~var(char|binary)$~', $type);
465
            $field->collationHidden = !preg_match('~(char|text|enum|set)$~', $type);
466
            $field->unsignedHidden = !(!$type || preg_match($this->driver->numberRegex(), $type));
467
            $field->onUpdateHidden = !preg_match('~timestamp|datetime~', $type);
468
            $field->onDeleteHidden = !preg_match('~`~', $type);
469
        }
470
        $options = [
471
            'hasAutoIncrement' => $hasAutoIncrement,
472
            'onUpdate' => ['CURRENT_TIMESTAMP' => 'CURRENT_TIMESTAMP'],
473
            'onDelete' => $this->driver->onActions(),
474
        ];
475
476
        $collations = $this->driver->collations();
477
        $engines = $this->driver->engines();
478
        $support = [
479
            'columns' => $this->driver->support('columns'),
480
            'comment' => $this->driver->support('comment'),
481
            'partitioning' => $this->driver->support('partitioning'),
482
            'move_col' => $this->driver->support('move_col'),
483
            'drop_col' => $this->driver->support('drop_col'),
484
        ];
485
486
        $foreignKeys = $this->foreignKeys;
487
        $unsigned = $this->driver->unsigned();
488
        // Give the var a better name
489
        $table = $status;
490
        return compact('table', 'foreignKeys', 'fields',
491
            'options', 'collations', 'engines', 'support', 'unsigned');
492
    }
493
494
    /**
495
     * Get fields for a new column
496
     *
497
     * @return TableFieldEntity
498
     */
499
    public function getTableField(): TableFieldEntity
500
    {
501
        $this->getForeignKeys();
502
        $field = new TableFieldEntity();
503
        $field->types = $this->getFieldTypes();
504
        return $field;
505
    }
506
507
    /**
508
     * Create or alter a table
509
     *
510
     * @param array  $values    The table values
511
     * @param string $table     The table name
512
     *
513
     * @return void
514
     */
515
    private function makeTableAttrs(array $values, string $table = '')
516
    {
517
        // From create.inc.php
518
        if ($values['autoIncrementCol']) {
519
            $values['fields'][$values['autoIncrementCol']]['autoIncrement'] = true;
520
        }
521
522
        $this->attrs = new TableEntity(trim($values['name']));
523
        $this->after = ' FIRST';
524
525
        $this->getForeignKeys();
526
527
        $this->fields = ($table !== '' ? $this->driver->fields($table) : []);
528
        foreach ($values['fields'] as $key => $field) {
529
            $orig = $field['orig'];
530
            $field = TableFieldEntity::make($field);
531
            $field->autoIncrement = ($key == $values['autoIncrementCol']);
532
            // Originally, deleted fields have the "field" field set to an empty string.
533
            // But in our implementation, the "name" field is not set.
534
            $this->addFieldToAttrs($field, $orig, $table);
535
        }
536
537
        // For now, partitioning is not implemented
538
        // $this->setPartitionAttr();
539
540
        $this->setValueAttrs($values);
541
    }
542
543
    /**
544
     * Create a table
545
     *
546
     * @param array  $values    The table values
547
     *
548
     * @return array
549
     */
550
    public function createTable(array $values): array
551
    {
552
        $this->makeTableAttrs($values);
553
        $success = $this->driver->createTable($this->attrs);
554
        $error = $this->driver->error();
555
        $message = $this->utils->trans->lang('Table has been created.');
556
557
        return compact('success', 'error', 'message');
558
    }
559
560
    /**
561
     * Alter a table
562
     *
563
     * @param string $table The table name
564
     * @param array $values The table values
565
     *
566
     * @return array
567
     * @throws Exception
568
     */
569
    public function alterTable(string $table, array $values): array
570
    {
571
        $this->tableStatus = $this->driver->tableStatus($table);
572
        if (!$this->tableStatus) {
573
            throw new Exception($this->utils->trans->lang('No tables.'));
574
        }
575
576
        $this->makeTableAttrs($values, $table);
577
        $success = $this->driver->alterTable($table, $this->attrs);
578
        $error = $this->driver->error();
579
        $message = $this->utils->trans->lang('Table has been altered.');
580
581
        return compact('success', 'error', 'message');
582
    }
583
584
    /**
585
     * Drop a table
586
     *
587
     * @param string $table     The table name
588
     *
589
     * @return array
590
     */
591
    public function dropTable(string $table): array
592
    {
593
        $success = $this->driver->dropTables([$table]);
594
        $error = $this->driver->error();
595
        $message = $this->utils->trans->lang('Table has been dropped.');
596
597
        return compact('success', 'message', 'error');
598
    }
599
}
600