Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.52% covered (success)
95.52%
128 / 134
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ShowTable
95.52% covered (success)
95.52%
128 / 134
60.00% covered (warning)
60.00%
3 / 5
34
0.00% covered (danger)
0.00%
0 / 1
 run
84.38% covered (warning)
84.38%
27 / 32
0.00% covered (danger)
0.00%
0 / 1
6.14
 getFields
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
10
 getIndexes
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 makeKeys
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 getForeignKeys
97.30% covered (success)
97.30%
36 / 37
0.00% covered (danger)
0.00%
0 / 1
9
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework Dev Commands Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\CLI\Commands;
11
12use Framework\CLI\CLI;
13use stdClass;
14
15/**
16 * Class ShowTable.
17 *
18 * @package dev-commands
19 */
20class ShowTable extends DatabaseCommand
21{
22    protected string $description = 'Shows a database table structure.';
23
24    public function run() : void
25    {
26        $this->setDatabase();
27        $table = $this->console->getArgument(0);
28        if (empty($table)) {
29            $table = CLI::prompt('Enter a table name');
30            CLI::newLine();
31        }
32        if (\str_contains($table, '.')) {
33            [$schema, $table] = \explode('.', $table, 2);
34            $this->getDatabase()->use($schema);
35        }
36        $show = $this->getDatabase()->query(
37            'SHOW TABLES LIKE ' . $this->getDatabase()->quote($table)
38        )->fetchArray();
39        if (empty($show)) {
40            CLI::beep();
41            CLI::error('Table not exist: ' . $table);
42            return;
43        }
44        $fields = $this->getFields($table);
45        CLI::write(
46            CLI::style('Table: ', 'white')
47            . CLI::style($table, 'yellow')
48        );
49        CLI::table($fields, \array_keys($fields[0]));
50        CLI::newLine();
51        $indexes = $this->getIndexes($table);
52        if ($indexes) {
53            CLI::write('Indexes', 'white');
54            CLI::table($indexes, \array_keys($indexes[0]));
55            CLI::newLine();
56        }
57        $foreignKeys = $this->getForeignKeys($table);
58        if ($foreignKeys) {
59            CLI::write('Foreign Keys', 'white');
60            CLI::table($foreignKeys, \array_keys($foreignKeys[0]));
61            CLI::newLine();
62        }
63    }
64
65    /**
66     * @param string $table
67     *
68     * @return array<int,array<string,string>>
69     */
70    protected function getFields(string $table) : array
71    {
72        $show = $this->getDatabase()->query(
73            'SHOW FULL COLUMNS FROM ' . $this->getDatabase()->protectIdentifier($table)
74        )->fetchArrayAll();
75        $columns = [];
76        foreach ($show as $row) {
77            \preg_match(
78                '~^([^( ]+)(?:\\((.+)\\))?( unsigned)?( zerofill)?$~',
79                $row['Type'],
80                $match
81            );
82            $columns[] = [
83                'field' => $row['Field'],
84                'full_type' => $row['Type'],
85                'type' => $match[1] ?? '',
86                'length' => $match[2] ?? '',
87                'unsigned' => \ltrim(($match[3] ?? '') . ($match[4] ?? '')),
88                'default' => $row['Default'] !== '' || \preg_match('~char|set~', $match[1])
89                    ? $row['Default'] : '',
90                'null' => $row['Null'] === 'YES',
91                'auto_increment' => ($row['Extra'] === 'auto_increment'),
92                'on_update' => \preg_match('~^on update (.+)~i', $row['Extra'], $match)
93                    ? $match[1] : '',
94                'collation' => $row['Collation'],
95                // @phpstan-ignore-next-line
96                'privileges' => \array_flip(\preg_split('~, *~', $row['Privileges'])),
97                'comment' => $row['Comment'],
98                'primary' => $row['Key'] === 'PRI',
99            ];
100        }
101        $cols = [];
102        foreach ($columns as $col) {
103            $cols[] = [
104                'Column' => $col['field'] . ($col['primary'] ? ' PRIMARY' : ''),
105                'Type' => $col['full_type']
106                    . ($col['collation'] ? ' ' . $col['collation'] : '')
107                    . ($col['auto_increment'] ? ' Auto Increment' : ''),
108                'Nullable' => $col['null'] ? 'Yes' : 'No',
109                'Default' => $col['default'],
110                'Comment' => $col['comment'],
111            ];
112        }
113        return $cols;
114    }
115
116    /**
117     * @param string $table
118     *
119     * @return array<int,array<string,string>>
120     */
121    protected function getIndexes(string $table) : array
122    {
123        $indexes = $this->getDatabase()->query(
124            'SHOW INDEX FROM ' . $this->getDatabase()->protectIdentifier($table)
125        )->fetchArrayAll();
126        $result = [];
127        foreach ($this->makeKeys($indexes) as $key) {
128            $result[] = [
129                'Name' => $key->name, // @phpstan-ignore-line
130                'Type' => $key->type, // @phpstan-ignore-line
131                'Columns' => \implode(', ', $key->fields), // @phpstan-ignore-line
132            ];
133        }
134        return $result;
135    }
136
137    /**
138     * @param array<mixed> $indexes
139     *
140     * @return array<object>
141     */
142    protected function makeKeys(array $indexes) : array
143    {
144        $keys = [];
145        foreach ($indexes as $index) {
146            if (empty($keys[$index['Key_name']])) {
147                $keys[$index['Key_name']] = new stdClass();
148                $keys[$index['Key_name']]->name = $index['Key_name'];
149                $type = 'UNIQUE';
150                if ($index['Key_name'] === 'PRIMARY') {
151                    $type = 'PRIMARY';
152                } elseif ($index['Index_type'] === 'FULLTEXT') {
153                    $type = 'FULLTEXT';
154                } elseif ($index['Non_unique']) {
155                    $type = $index['Index_type'] === 'SPATIAL' ? 'SPATIAL' : 'INDEX';
156                }
157                $keys[$index['Key_name']]->type = $type;
158            }
159            $keys[$index['Key_name']]->fields[] = $index['Column_name'];
160        }
161        return $keys;
162    }
163
164    /**
165     * @param string $table
166     *
167     * @return array<int,array<string,string>>
168     */
169    protected function getForeignKeys(string $table) : array
170    {
171        $show = $this->getDatabase()->query(
172            'SHOW CREATE TABLE ' . $this->getDatabase()->protectIdentifier($table)
173        )->fetchArray();
174        if ( ! $show) {
175            return [];
176        }
177        $createTable = $show['Create Table'];
178        $onActions = 'RESTRICT|NO ACTION|CASCADE|SET NULL|SET DEFAULT';
179        $pattern = '`(?:[^`]|``)+`';
180        \preg_match_all(
181            "~CONSTRAINT ({$pattern}) FOREIGN KEY ?\\(((?:{$pattern},? ?)+)\\) REFERENCES ({$pattern})(?:\\.({$pattern}))? \\(((?:{$pattern},? ?)+)\\)(?: ON DELETE ({$onActions}))?(?: ON UPDATE ({$onActions}))?~",
182            $createTable, // @phpstan-ignore-line
183            $matches,
184            \PREG_SET_ORDER
185        );
186        $foreignKeys = [];
187        foreach ($matches as $match) {
188            \preg_match_all("~{$pattern}~", $match[2], $source);
189            \preg_match_all("~{$pattern}~", $match[5], $target);
190            $foreignKeys[] = [
191                'index' => \str_replace('`', '', $match[1]),
192                'source' => \str_replace('`', '', $source[0][0]),
193                'database' => \str_replace('`', '', $match[4] !== '' ? $match[3] : $match[4]),
194                'table' => \str_replace('`', '', $match[4] !== '' ? $match[4] : $match[3]),
195                'field' => \str_replace('`', '', $target[0][0]),
196                'on_delete' => ( ! empty($match[6]) ? $match[6] : 'RESTRICT'),
197                'on_update' => ( ! empty($match[7]) ? $match[7] : 'RESTRICT'),
198            ];
199        }
200        $fks = [];
201        foreach ($foreignKeys as $fk) {
202            $fks[] = [
203                'Source' => $fk['source'],
204                'Target' => ( ! empty($fk['database']) ? $fk['database'] . '.' : '')
205                    . $fk['table'] . '(' . $fk['field'] . ')',
206                'ON DELETE' => $fk['on_delete'],
207                'ON UPDATE' => $fk['on_update'],
208            ];
209        }
210        return $fks;
211    }
212}