Passed
Push — main ( c0ed3d...4f3c5a )
by Thierry
08:13
created

TableAdmin::getTableInfo()   B

Complexity

Conditions 7
Paths 20

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 22
c 0
b 0
f 0
nc 20
nop 1
dl 0
loc 39
rs 8.6346
1
<?php
2
3
namespace Lagdo\DbAdmin\DbAdmin;
4
5
use Lagdo\DbAdmin\Driver\Entity\TableEntity;
6
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
7
use Lagdo\DbAdmin\Driver\Entity\ForeignKeyEntity;
8
9
use Exception;
10
11
/**
12
 * Admin table functions
13
 */
14
class TableAdmin extends AbstractAdmin
15
{
16
    /**
17
     * The current table status
18
     *
19
     * @var mixed
20
     */
21
    protected $tableStatus = null;
22
23
    /**
24
     * @var array
25
     */
26
    protected $referencableTables = [];
27
28
    /**
29
     * @var array
30
     */
31
    protected $foreignKeys = [];
32
33
    /**
34
     * Get the current table status
35
     *
36
     * @param string $table
37
     *
38
     * @return mixed
39
     */
40
    protected function status(string $table)
41
    {
42
        if (!$this->tableStatus) {
43
            $this->tableStatus = $this->driver->tableStatusOrName($table, true);
44
        }
45
        return $this->tableStatus;
46
    }
47
48
    /**
49
     * Print links after select heading
50
     * Copied from selectLinks() in adminer.inc.php
51
     *
52
     * @param string $set New item options, NULL for no new item
53
     *
54
     * @return array
55
     */
56
    protected function getTableLinks($set = null)
57
    {
58
        $links = [
59
            'select' => $this->trans->lang('Select data'),
60
        ];
61
        if ($this->driver->support('table') || $this->driver->support('indexes')) {
62
            $links['table'] = $this->trans->lang('Show structure');
63
        }
64
        if ($this->driver->support('table')) {
65
            $links['alter'] = $this->trans->lang('Alter table');
66
        }
67
        if ($set !== null) {
68
            $links['edit'] = $this->trans->lang('New item');
69
        }
70
        // $links['docs'] = \doc_link([$this->driver->jush() => $this->driver->tableHelp($name)], '?');
71
72
        return $links;
73
    }
74
75
    /**
76
     * Get details about a table
77
     *
78
     * @param string $table     The table name
79
     *
80
     * @return array
81
     */
82
    public function getTableInfo(string $table)
83
    {
84
        $mainActions = [
85
            'edit-table' => $this->trans->lang('Alter table'),
86
            'drop-table' => $this->trans->lang('Drop table'),
87
            'select-table' => $this->trans->lang('Select'),
88
            'insert-table' => $this->trans->lang('New item'),
89
        ];
90
91
        // From table.inc.php
92
        $status = $this->status($table);
93
        $name = $this->util->tableName($status);
94
        $title = $this->trans->lang('Table') . ': ' . ($name != '' ? $name : $this->util->html($table));
95
96
        $comment = $status->comment;
97
98
        $tabs = [
99
            'fields' => $this->trans->lang('Columns'),
100
            // 'indexes' => $this->trans->lang('Indexes'),
101
            // 'foreign-keys' => $this->trans->lang('Foreign keys'),
102
            // 'triggers' => $this->trans->lang('Triggers'),
103
        ];
104
        if ($this->driver->isView($status)) {
105
            if ($this->driver->support('view_trigger')) {
106
                $tabs['triggers'] = $this->trans->lang('Triggers');
107
            }
108
        } else {
109
            if ($this->driver->support('indexes')) {
110
                $tabs['indexes'] = $this->trans->lang('Indexes');
111
            }
112
            if ($this->driver->supportForeignKeys($status)) {
113
                $tabs['foreign-keys'] = $this->trans->lang('Foreign keys');
114
            }
115
            if ($this->driver->support('trigger')) {
116
                $tabs['triggers'] = $this->trans->lang('Triggers');
117
            }
118
        }
119
120
        return \compact('mainActions', 'title', 'comment', 'tabs');
121
    }
122
123
    /**
124
     * Get the fields of a table
125
     *
126
     * @param string $table     The table name
127
     *
128
     * @return array
129
     */
130
    public function getTableFields(string $table)
131
    {
132
        // From table.inc.php
133
        $fields = $this->driver->fields($table);
134
        if (empty($fields)) {
135
            throw new Exception($this->driver->error());
136
        }
137
138
        $mainActions = $this->getTableLinks();
139
140
        $tabs = [
141
            'fields' => $this->trans->lang('Columns'),
142
            // 'indexes' => $this->trans->lang('Indexes'),
143
            // 'foreign-keys' => $this->trans->lang('Foreign keys'),
144
            // 'triggers' => $this->trans->lang('Triggers'),
145
        ];
146
        if ($this->driver->support('indexes')) {
147
            $tabs['indexes'] = $this->trans->lang('Indexes');
148
        }
149
        if ($this->driver->supportForeignKeys($this->status($table))) {
150
            $tabs['foreign-keys'] = $this->trans->lang('Foreign keys');
151
        }
152
        if ($this->driver->support('trigger')) {
153
            $tabs['triggers'] = $this->trans->lang('Triggers');
154
        }
155
156
        $headers = [
157
            $this->trans->lang('Name'),
158
            $this->trans->lang('Type'),
159
            $this->trans->lang('Collation'),
160
        ];
161
        $hasComment = $this->driver->support('comment');
162
        if ($hasComment) {
163
            $headers[] = $this->trans->lang('Comment');
164
        }
165
166
        $details = [];
167
        foreach ($fields as $field) {
168
            $type = $this->util->html($field->fullType);
169
            if ($field->null) {
170
                $type .= ' <i>nullable</i>'; // ' <i>NULL</i>';
171
            }
172
            if ($field->autoIncrement) {
173
                $type .= ' <i>' . $this->trans->lang('Auto Increment') . '</i>';
174
            }
175
            if ($field->default !== '') {
176
                $type .= /*' ' . $this->trans->lang('Default value') .*/ ' [<b>' . $this->util->html($field->default) . '</b>]';
177
            }
178
            $detail = [
179
                'name' => $this->util->html($field->name),
180
                'type' => $type,
181
                'collation' => $this->util->html($field->collation),
182
            ];
183
            if ($hasComment) {
184
                $detail['comment'] = $this->util->html($field->comment);
185
            }
186
187
            $details[] = $detail;
188
        }
189
190
        return \compact('mainActions', 'headers', 'details');
191
    }
192
193
    /**
194
     * Get the indexes of a table
195
     *
196
     * @param string $table     The table name
197
     *
198
     * @return array|null
199
     */
200
    public function getTableIndexes(string $table)
201
    {
202
        if (!$this->driver->support('indexes')) {
203
            return null;
204
        }
205
206
        // From table.inc.php
207
        $indexes = $this->driver->indexes($table);
208
        $mainActions = [
209
            'create' => $this->trans->lang('Alter indexes'),
210
        ];
211
212
        $headers = [
213
            $this->trans->lang('Name'),
214
            $this->trans->lang('Type'),
215
            $this->trans->lang('Column'),
216
        ];
217
218
        $details = [];
219
        // From adminer.inc.php
220
        foreach ($indexes as $name => $index) {
221
            \ksort($index->columns); // enforce correct columns order
222
            $print = [];
223
            foreach ($index->columns as $key => $val) {
224
                $value = '<i>' . $this->util->html($val) . '</i>';
225
                if (\array_key_exists($key, $index->lengths)) {
226
                    $value .= '(' . $index->lengths[$key] . ')';
227
                }
228
                if (\array_key_exists($key, $index->descs)) {
229
                    $value .= ' DESC';
230
                }
231
                $print[] = $value;
232
            }
233
            $details[] = [
234
                'name' => $this->util->html($name),
235
                'type' => $index->type,
236
                'desc' => \implode(', ', $print),
237
            ];
238
        }
239
240
        return \compact('mainActions', 'headers', 'details');
241
    }
242
243
    /**
244
     * Get the foreign keys of a table
245
     *
246
     * @param string $table     The table name
247
     *
248
     * @return array|null
249
     */
250
    public function getTableForeignKeys(string $table)
251
    {
252
        $status = $this->status($table);
253
        if (!$this->driver->supportForeignKeys($status)) {
254
            return null;
255
        }
256
257
        // From table.inc.php
258
        $mainActions = [
259
            $this->trans->lang('Add foreign key'),
260
        ];
261
262
        $headers = [
263
            $this->trans->lang('Name'),
264
            $this->trans->lang('Source'),
265
            $this->trans->lang('Target'),
266
            $this->trans->lang('ON DELETE'),
267
            $this->trans->lang('ON UPDATE'),
268
        ];
269
270
        $foreignKeys = $this->driver->foreignKeys($table);
271
        $details = [];
272
        // From table.inc.php
273
        foreach ($foreignKeys as $name => $foreignKey) {
274
            $target = '';
275
            if ($foreignKey->database != '') {
276
                $target .= '<b>' . $this->util->html($foreignKey->database) . '</b>.';
277
            }
278
            if ($foreignKey->schema != '') {
279
                $target .= '<b>' . $this->util->html($foreignKey->schema) . '</b>.';
280
            }
281
            $target = $this->util->html($foreignKey->table) .
282
                '(' . \implode(', ', \array_map(function ($key) {
283
                    return $this->util->html($key);
284
                }, $foreignKey->target)) . ')';
285
            $details[] = [
286
                'name' => $this->util->html($name),
287
                'source' => '<i>' . \implode(
288
                    '</i>, <i>',
289
                    \array_map(function ($key) {
290
                        return $this->util->html($key);
291
                    }, $foreignKey->source)
292
                ) . '</i>',
293
                'target' => $target,
294
                'onDelete' => $this->util->html($foreignKey->onDelete),
295
                'onUpdate' => $this->util->html($foreignKey->onUpdate),
296
            ];
297
        }
298
299
        return \compact('mainActions', 'headers', 'details');
300
    }
301
302
    /**
303
     * Get the triggers of a table
304
     *
305
     * @param string $table     The table name
306
     *
307
     * @return array|null
308
     */
309
    public function getTableTriggers(string $table)
310
    {
311
        if (!$this->driver->support('trigger')) {
312
            return null;
313
        }
314
315
        $mainActions = [
316
            $this->trans->lang('Add trigger'),
317
        ];
318
319
        $headers = [
320
            $this->trans->lang('Name'),
321
            '&nbsp;',
322
            '&nbsp;',
323
            '&nbsp;',
324
        ];
325
326
        $details = [];
327
        // From table.inc.php
328
        $triggers = $this->driver->triggers($table);
329
        foreach ($triggers as $name => $trigger) {
330
            $details[] = [
331
                $this->util->html($trigger->timing),
332
                $this->util->html($trigger->event),
333
                $this->util->html($name),
334
                $this->trans->lang('Alter'),
335
            ];
336
        }
337
338
        return \compact('mainActions', 'headers', 'details');
339
    }
340
341
    /**
342
     * Get foreign keys
343
     *
344
     * @param string $table     The table name
345
     *
346
     * @return void
347
     */
348
    private function getForeignKeys(string $table = '')
349
    {
350
        $this->referencableTables = $this->driver->referencableTables($table);
351
        $this->foreignKeys = [];
352
        foreach ($this->referencableTables as $tableName => $field) {
353
            $name = \str_replace('`', '``', $tableName) . '`' . \str_replace('`', '``', $field->name);
354
            // not escapeId() - used in JS
355
            $this->foreignKeys[$name] = $tableName;
356
        }
357
    }
358
359
    /**
360
     * Get field types
361
     *
362
     * @param string $type  The type name
363
     *
364
     * @return array
365
     */
366
    public function getFieldTypes(string $type = '')
367
    {
368
        // From includes/editing.inc.php
369
        $extraTypes = [];
370
        if ($type && !$this->driver->typeExists($type) && !isset($this->foreignKeys[$type]) &&
371
            !\array_key_exists($this->trans->lang('Current'), $extraTypes)) {
372
            $extraTypes[$this->trans->lang('Current')] = [$type];
373
        }
374
        if (!empty($this->foreignKeys)) {
375
            $this->driver->setStructuredType($this->trans->lang('Foreign keys'), $this->foreignKeys);
376
        }
377
        return \array_merge($extraTypes, $this->driver->structuredTypes());
378
    }
379
380
    /**
381
     * Get required data for create/update on tables
382
     *
383
     * @param string $table The table name
384
     *
385
     * @return array
386
     */
387
    public function getTableData(string $table = '')
388
    {
389
        $mainActions = [
390
            'table-save' => $this->trans->lang('Save'),
391
            'table-cancel' => $this->trans->lang('Cancel'),
392
        ];
393
394
        // From create.inc.php
395
        $status = [];
396
        $fields = [];
397
        if ($table !== '') {
398
            $status = $this->driver->tableStatus($table);
399
            if (!$status) {
400
                throw new Exception($this->trans->lang('No tables.'));
401
            }
402
            $fields = $this->driver->fields($table);
403
        }
404
405
        $this->getForeignKeys($table);
406
407
        $hasAutoIncrement = false;
408
        foreach ($fields as &$field) {
409
            $hasAutoIncrement = $hasAutoIncrement || $field->autoIncrement;
410
            $field->hasDefault = $field->default !== null;
411
            if (\preg_match('~^CURRENT_TIMESTAMP~i', $field->onUpdate)) {
412
                $field->onUpdate = 'CURRENT_TIMESTAMP';
413
            }
414
415
            $type = $field->type;
416
            $field->types = $this->getFieldTypes($type);
417
            $field->lengthRequired = !$field->length && \preg_match('~var(char|binary)$~', $type);
418
            $field->collationHidden = !\preg_match('~(char|text|enum|set)$~', $type);
419
            $field->unsignedHidden = !(!$type || \preg_match($this->driver->numberRegex(), $type));
420
            $field->onUpdateHidden = !\preg_match('~timestamp|datetime~', $type);
421
            $field->onDeleteHidden = !\preg_match('~`~', $type);
422
        }
423
        $options = [
424
            'hasAutoIncrement' => $hasAutoIncrement,
425
            'onUpdate' => ['CURRENT_TIMESTAMP'],
426
            'onDelete' => $this->driver->onActions(),
427
        ];
428
429
        $collations = $this->driver->collations();
430
        $engines = $this->driver->engines();
431
        $support = [
432
            'columns' => $this->driver->support('columns'),
433
            'comment' => $this->driver->support('comment'),
434
            'partitioning' => $this->driver->support('partitioning'),
435
            'move_col' => $this->driver->support('move_col'),
436
            'drop_col' => $this->driver->support('drop_col'),
437
        ];
438
439
        $foreignKeys = $this->foreignKeys;
440
        $unsigned = $this->driver->unsigned();
441
        // Give the var a better name
442
        $table = $status;
443
        return \compact('mainActions', 'table', 'foreignKeys', 'fields',
444
            'options', 'collations', 'engines', 'support', 'unsigned');
445
    }
446
447
    /**
448
     * Get fields for a new column
449
     *
450
     * @return TableFieldEntity
451
     */
452
    public function getTableField()
453
    {
454
        $this->getForeignKeys();
455
        $field = new TableFieldEntity();
456
        $field->types = $this->getFieldTypes();
457
        return $field;
458
    }
459
460
    /**
461
     * Create or alter a table
462
     *
463
     * @param array  $values    The table values
464
     * @param string $table     The table name
465
     * @param array $origFields The table fields
466
     *
467
     * @return TableEntity
468
     */
469
    private function getTableAttrs(array $values, string $table = '', array $origFields = [])
0 ignored issues
show
Unused Code introduced by
The parameter $origFields is not used and could be removed. ( Ignorable by Annotation )

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

469
    private function getTableAttrs(array $values, string $table = '', /** @scrutinizer ignore-unused */ array $origFields = [])

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
470
    {
471
        // From create.inc.php
472
        $values['fields'] = (array)$values['fields'];
473
        if ($values['autoIncrementCol']) {
474
            $values['fields'][$values['autoIncrementCol']]['autoIncrement'] = true;
475
        }
476
477
        $tableAttrs = new TableEntity(\trim($values['name']));
478
        $after = ' FIRST';
479
480
        $this->getForeignKeys();
481
482
        $origFields = $table !== '' ? $this->driver->fields($table) : [];
483
        foreach ($values['fields'] as $key => $field) {
484
            $orig = $field['orig'];
485
            $origField = $origFields[$orig] ?? null;
486
            $field = TableFieldEntity::make($field);
487
            $foreignKey = $this->foreignKeys[$field->type] ?? null;
488
            //! can collide with user defined type
489
            $typeField = ($foreignKey === null ? $field :
490
                TableFieldEntity::make($this->referencableTables[$foreignKey]));
491
            // Originally, deleted fields have the "field" field set to an empty string.
492
            // But in our implementation, the "name" field is not set.
493
            if ($field->name != '') {
494
                $field->autoIncrement = ($key == $values['autoIncrementCol']);
495
496
                $processedField = $this->util->processField($field, $typeField);
497
                if ($orig === '') {
498
                    $tableAttrs->fields[] = [$orig, $processedField, $after];
0 ignored issues
show
Bug introduced by
The property fields does not seem to exist on Lagdo\DbAdmin\Driver\Entity\TableEntity.
Loading history...
499
                } elseif ($origField !== null && $field->changed($origField)) {
500
                    $tableAttrs->edited[] = [$orig, $processedField, $after];
0 ignored issues
show
Bug introduced by
The property edited does not seem to exist on Lagdo\DbAdmin\Driver\Entity\TableEntity.
Loading history...
501
                }
502
                if ($foreignKey !== null) {
503
                    $fkey = new ForeignKeyEntity();
504
                    $fkey->table = $this->foreignKeys[$field->type];
505
                    $fkey->source = [$field->name];
506
                    $fkey->target = [$typeField['field']];
507
                    $fkey->onDelete = $field->onDelete;
508
                    $tableAttrs->foreign[$this->driver->escapeId($field->name)] =
0 ignored issues
show
Bug introduced by
The property foreign does not seem to exist on Lagdo\DbAdmin\Driver\Entity\TableEntity.
Loading history...
509
                        ($table != '' && $this->driver->jush() != 'sqlite' ? 'ADD' : ' ') .
510
                        $this->driver->formatForeignKey($fkey);
511
                }
512
                $after = ' AFTER ' . $this->driver->escapeId($field->name);
513
            } elseif ($orig !== '') {
514
                // A missing "name" field and a not empty "orig" field means the column is to be dropped.
515
                $tableAttrs->dropped[] = $orig;
0 ignored issues
show
Bug introduced by
The property dropped does not seem to exist on Lagdo\DbAdmin\Driver\Entity\TableEntity.
Loading history...
516
            }
517
            if ($orig !== '') {
518
                $after = '';
519
            }
520
        }
521
522
        // For now, partitioning is not implemented
523
        $tableAttrs->partitioning = '';
0 ignored issues
show
Bug introduced by
The property partitioning does not seem to exist on Lagdo\DbAdmin\Driver\Entity\TableEntity.
Loading history...
524
        // if($partition_by[$values['partition_by']])
525
        // {
526
        //     $partitions = [];
527
        //     if($values['partition_by'] == 'RANGE' || $values['partition_by'] == 'LIST')
528
        //     {
529
        //         foreach(\array_filter($values['partition_names']) as $key => $val)
530
        //         {
531
        //             $value = $values['partition_values'][$key];
532
        //             $partitions[] = "\n  PARTITION " . $this->driver->escapeId($val) .
533
        //                 ' VALUES ' . ($values['partition_by'] == 'RANGE' ? 'LESS THAN' : 'IN') .
534
        //                 ($value != '' ? ' ($value)' : ' MAXVALUE'); //! SQL injection
535
        //         }
536
        //     }
537
        //     $tableAttrs->partitioning .= "\nPARTITION BY $values[partition_by]($values[partition])" .
538
        //         ($partitions // $values['partition'] can be expression, not only column
539
        //         ? ' (' . \implode(',', $partitions) . "\n)"
540
        //         : ($values['partitions'] ? ' PARTITIONS ' . (+$values['partitions']) : '')
541
        //     );
542
        // }
543
        // elseif($this->driver->support('partitioning') &&
544
        //     \preg_match('~partitioned~', $this->tableStatus->Create_options))
545
        // {
546
        //     $tableAttrs->partitioning .= "\nREMOVE PARTITIONING";
547
        // }
548
549
        foreach (['comment', 'engine', 'collation'] as $attr) {
550
            $tableAttrs->$attr = !empty($values[$attr]) ? $values[$attr] : '';
551
            if ($this->tableStatus != null) {
552
                // No change.
553
                if ($tableAttrs->$attr == $this->tableStatus->$attr) {
554
                    $tableAttrs->$attr = '';
555
                }
556
            }
557
        }
558
559
        $tableAttrs->autoIncrement = \intval($this->util->number($this->util->input()->getAutoIncrementStep()));
0 ignored issues
show
Bug introduced by
The property autoIncrement does not seem to exist on Lagdo\DbAdmin\Driver\Entity\TableEntity.
Loading history...
560
561
        return $tableAttrs;
562
    }
563
564
    /**
565
     * Create a table
566
     *
567
     * @param array  $values    The table values
568
     *
569
     * @return array
570
     */
571
    public function createTable(array $values)
572
    {
573
        $tableAttrs = $this->getTableAttrs($values);
574
        $success = $this->driver->createTable($tableAttrs);
0 ignored issues
show
Bug introduced by
The method createTable() does not exist on Lagdo\DbAdmin\Driver\DriverInterface. ( Ignorable by Annotation )

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

574
        /** @scrutinizer ignore-call */ 
575
        $success = $this->driver->createTable($tableAttrs);

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...
575
        $error = $this->driver->error();
576
        $message = $this->trans->lang('Table has been created.');
577
578
        return \compact('success', 'error', 'message');
579
    }
580
581
    /**
582
     * Alter a table
583
     *
584
     * @param string $table     The table name
585
     * @param array  $values    The table values
586
     *
587
     * @return array
588
     */
589
    public function alterTable(string $table, array $values)
590
    {
591
        $this->tableStatus = $this->driver->tableStatus($table);
592
        if (!$this->tableStatus) {
593
            throw new Exception($this->trans->lang('No tables.'));
594
        }
595
596
        $tableAttrs = $this->getTableAttrs($values, $table);
597
        $success = $this->driver->alterTable($table, $tableAttrs);
0 ignored issues
show
Bug introduced by
The call to Lagdo\DbAdmin\Driver\Db\...Interface::alterTable() has too few arguments starting with fields. ( Ignorable by Annotation )

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

597
        /** @scrutinizer ignore-call */ 
598
        $success = $this->driver->alterTable($table, $tableAttrs);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
$tableAttrs of type Lagdo\DbAdmin\Driver\Entity\TableEntity is incompatible with the type string expected by parameter $name of Lagdo\DbAdmin\Driver\Db\...Interface::alterTable(). ( Ignorable by Annotation )

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

597
        $success = $this->driver->alterTable($table, /** @scrutinizer ignore-type */ $tableAttrs);
Loading history...
598
        $error = $this->driver->error();
599
        $message = $this->trans->lang('Table has been altered.');
600
601
        return \compact('success', 'error', 'message');
602
    }
603
604
    /**
605
     * Drop a table
606
     *
607
     * @param string $table     The table name
608
     *
609
     * @return array
610
     */
611
    public function dropTable(string $table)
612
    {
613
        $success = $this->driver->dropTables([$table]);
614
615
        $error = $this->driver->error();
616
617
        $message = $this->trans->lang('Table has been dropped.');
618
619
        return \compact('success', 'message', 'error');
620
    }
621
}
622