Passed
Push — main ( 221746...fcb00c )
by Thierry
03:00 queued 44s
created

TableAdmin::status()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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