Completed
Push — master ( 18309a...233d72 )
by Maurício
01:40 queued 25s
created

RelationController   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Test Coverage

Coverage 23.18%

Importance

Changes 0
Metric Value
eloc 199
dl 0
loc 369
ccs 35
cts 151
cp 0.2318
rs 8.8
c 0
b 0
f 0
wmc 45

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A getDropdownValueForTable() 0 25 4
A updateForDisplayField() 0 9 1
F __invoke() 0 171 23
B updateForForeignKeys() 0 48 7
A updateForInternalRelation() 0 25 2
B getDropdownValueForDatabase() 0 29 7

How to fix   Complexity   

Complex Class

Complex classes like RelationController 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 RelationController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\Controllers\Table;
6
7
use PhpMyAdmin\ConfigStorage\Features\DisplayFeature;
8
use PhpMyAdmin\ConfigStorage\Features\RelationFeature;
9
use PhpMyAdmin\ConfigStorage\Relation;
10
use PhpMyAdmin\Controllers\AbstractController;
11
use PhpMyAdmin\Core;
12
use PhpMyAdmin\DatabaseInterface;
13
use PhpMyAdmin\Html\Generator;
14
use PhpMyAdmin\Index;
15
use PhpMyAdmin\ResponseRenderer;
16
use PhpMyAdmin\Routing;
17
use PhpMyAdmin\Table;
18
use PhpMyAdmin\Template;
19
use PhpMyAdmin\Util;
20
use PhpMyAdmin\Utils\ForeignKey;
21
22
use function __;
23
use function array_key_exists;
24
use function array_keys;
25
use function array_values;
26
use function mb_strtoupper;
27
use function md5;
28
use function strtoupper;
29
use function uksort;
30
use function usort;
31
32
/**
33
 * Display table relations for viewing and editing.
34
 *
35
 * Includes phpMyAdmin relations and InnoDB relations.
36
 */
37
final class RelationController extends AbstractController
38
{
39
    /** @var Relation */
40
    private $relation;
41
42
    /** @var DatabaseInterface */
43
    private $dbi;
44
45 16
    public function __construct(
46
        ResponseRenderer $response,
47
        Template $template,
48
        Relation $relation,
49
        DatabaseInterface $dbi
50
    ) {
51 16
        parent::__construct($response, $template);
52 16
        $this->relation = $relation;
53 16
        $this->dbi = $dbi;
54 4
    }
55
56
    /**
57
     * Index
58
     */
59
    public function __invoke(): void
60
    {
61
        $route = Routing::getCurrentRoute();
62
63
        $options = [
64
            'CASCADE' => 'CASCADE',
65
            'SET_NULL' => 'SET NULL',
66
            'NO_ACTION' => 'NO ACTION',
67
            'RESTRICT' => 'RESTRICT',
68
        ];
69
70
        $table = $this->dbi->getTable($GLOBALS['db'], $GLOBALS['table']);
71
        $storageEngine = mb_strtoupper((string) $table->getStatusInfo('Engine'));
72
73
        $relationParameters = $this->relation->getRelationParameters();
74
75
        $relations = [];
76
        if ($relationParameters->relationFeature !== null) {
77
            $relations = $this->relation->getForeigners($GLOBALS['db'], $GLOBALS['table'], '', 'internal');
78
        }
79
80
        $relationsForeign = [];
81
        if (ForeignKey::isSupported($storageEngine)) {
82
            $relationsForeign = $this->relation->getForeigners($GLOBALS['db'], $GLOBALS['table'], '', 'foreign');
83
        }
84
85
        // Send table of column names to populate corresponding dropdowns depending
86
        // on the current selection
87
        if (isset($_POST['getDropdownValues']) && $_POST['getDropdownValues'] === 'true') {
88
            // if both db and table are selected
89
            if (isset($_POST['foreignTable'])) {
90
                $this->getDropdownValueForTable();
91
            } else { // if only the db is selected
92
                $this->getDropdownValueForDatabase($storageEngine);
93
            }
94
95
            return;
96
        }
97
98
        $this->addScriptFiles(['table/relation.js', 'indexes.js']);
99
100
        // Set the database
101
        $this->dbi->selectDb($GLOBALS['db']);
102
103
        // updates for Internal relations
104
        if (isset($_POST['destination_db']) && $relationParameters->relationFeature !== null) {
105
            $this->updateForInternalRelation($table, $relationParameters->relationFeature, $relations);
106
        }
107
108
        // updates for foreign keys
109
        $this->updateForForeignKeys($table, $options, $relationsForeign);
110
111
        // Updates for display field
112
        if ($relationParameters->displayFeature !== null && isset($_POST['display_field'])) {
113
            $this->updateForDisplayField($table, $relationParameters->displayFeature);
114
        }
115
116
        // If we did an update, refresh our data
117
        if (isset($_POST['destination_db']) && $relationParameters->relationFeature !== null) {
118
            $relations = $this->relation->getForeigners($GLOBALS['db'], $GLOBALS['table'], '', 'internal');
119
        }
120
121
        if (isset($_POST['destination_foreign_db']) && ForeignKey::isSupported($storageEngine)) {
122
            $relationsForeign = $this->relation->getForeigners($GLOBALS['db'], $GLOBALS['table'], '', 'foreign');
123
        }
124
125
        /**
126
         * Dialog
127
         */
128
        // Now find out the columns of our $table
129
        // need to use DatabaseInterface::QUERY_BUFFERED with $this->dbi->numRows()
130
        // in mysqli
131
        $columns = $this->dbi->getColumns($GLOBALS['db'], $GLOBALS['table']);
132
133
        $column_array = [];
134
        $column_hash_array = [];
135
        $column_array[''] = '';
136
        foreach ($columns as $column) {
137
            if (strtoupper($storageEngine) !== 'INNODB' && empty($column['Key'])) {
138
                continue;
139
            }
140
141
            $column_array[$column['Field']] = $column['Field'];
142
            $column_hash_array[$column['Field']] = md5($column['Field']);
143
        }
144
145
        if ($GLOBALS['cfg']['NaturalOrder']) {
146
            uksort($column_array, 'strnatcasecmp');
147
        }
148
149
        $foreignKeyRow = '';
150
        $existrelForeign = array_key_exists('foreign_keys_data', $relationsForeign)
151
            ? $relationsForeign['foreign_keys_data']
152
            : [];
153
        $i = 0;
154
155
        foreach ($existrelForeign as $key => $oneKey) {
156
            $foreignDb = $oneKey['ref_db_name'] ?? $GLOBALS['db'];
157
            $foreignTable = false;
158
            if ($foreignDb) {
159
                $foreignTable = $oneKey['ref_table_name'] ?? false;
160
                $tables = $this->relation->getTables($foreignDb, $storageEngine);
161
            } else {
162
                $tables = $this->relation->getTables($GLOBALS['db'], $storageEngine);
163
            }
164
165
            $uniqueColumns = [];
166
            if ($foreignDb && $foreignTable) {
167
                $tableObject = Table::get(
168
                    $foreignTable,
169
                    $foreignDb
170
                );
171
                $uniqueColumns = $tableObject->getUniqueColumns(false, false);
172
            }
173
174
            $foreignKeyRow .= $this->template->render('table/relation/foreign_key_row', [
175
                'i' => $i,
176
                'one_key' => $oneKey,
177
                'column_array' => $column_array,
178
                'options_array' => $options,
179
                'tbl_storage_engine' => $storageEngine,
180
                'db' => $GLOBALS['db'],
181
                'table' => $GLOBALS['table'],
182
                'url_params' => $GLOBALS['urlParams'],
183
                'databases' => $GLOBALS['dblist']->databases,
184
                'foreign_db' => $foreignDb,
185
                'foreign_table' => $foreignTable,
186
                'unique_columns' => $uniqueColumns,
187
                'tables' => $tables,
188
            ]);
189
            $i++;
190
        }
191
192
        $tables = $this->relation->getTables($GLOBALS['db'], $storageEngine);
193
        $foreignKeyRow .= $this->template->render('table/relation/foreign_key_row', [
194
            'i' => $i,
195
            'one_key' => [],
196
            'column_array' => $column_array,
197
            'options_array' => $options,
198
            'tbl_storage_engine' => $storageEngine,
199
            'db' => $GLOBALS['db'],
200
            'table' => $GLOBALS['table'],
201
            'url_params' => $GLOBALS['urlParams'],
202
            'databases' => $GLOBALS['dblist']->databases,
203
            'foreign_db' => false,
204
            'foreign_table' => false,
205
            'unique_columns' => [],
206
            'tables' => $tables,
207
        ]);
208
209
        // common form
210
        $engine = $this->dbi->getTable($GLOBALS['db'], $GLOBALS['table'])->getStorageEngine();
211
        $this->render('table/relation/common_form', [
212
            'is_foreign_key_supported' => ForeignKey::isSupported($engine),
213
            'db' => $GLOBALS['db'],
214
            'table' => $GLOBALS['table'],
215
            'relation_parameters' => $relationParameters,
216
            'tbl_storage_engine' => $storageEngine,
217
            'existrel' => $relations,
218
            'existrel_foreign' => $existrelForeign,
219
            'options_array' => $options,
220
            'column_array' => $column_array,
221
            'column_hash_array' => $column_hash_array,
222
            'save_row' => array_values($columns),
223
            'url_params' => $GLOBALS['urlParams'],
224
            'databases' => $GLOBALS['dblist']->databases,
225
            'dbi' => $this->dbi,
226
            'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'],
227
            'route' => $route,
228
            'display_field' => $this->relation->getDisplayField($GLOBALS['db'], $GLOBALS['table']),
229
            'foreign_key_row' => $foreignKeyRow,
230
        ]);
231
    }
232
233
    /**
234
     * Update for display field
235
     */
236
    private function updateForDisplayField(Table $table, DisplayFeature $displayFeature): void
237
    {
238
        $table->updateDisplayField($_POST['display_field'], $displayFeature);
239
240
        $this->response->addHTML(
241
            Generator::getMessage(
242
                __('Display column was successfully updated.'),
243
                '',
244
                'success'
245
            )
246
        );
247
    }
248
249
    /**
250
     * Update for FK
251
     *
252
     * @param Table $table            Table
253
     * @param array $options          Options
254
     * @param array $relationsForeign External relations
255
     */
256
    private function updateForForeignKeys(Table $table, array $options, array $relationsForeign): void
257
    {
258
        $multi_edit_columns_name = $_POST['foreign_key_fields_name'] ?? null;
259
        $preview_sql_data = '';
260
        $seen_error = false;
261
262
        // (for now, one index name only; we keep the definitions if the
263
        // foreign db is not the same)
264
        if (
265
            isset($_POST['destination_foreign_db'], $_POST['destination_foreign_table'])
266
            && isset($_POST['destination_foreign_column'])
267
        ) {
268
            [
269
                $html,
270
                $preview_sql_data,
271
                $display_query,
272
                $seen_error,
273
            ] = $table->updateForeignKeys(
274
                $_POST['destination_foreign_db'],
275
                $multi_edit_columns_name,
0 ignored issues
show
Bug introduced by
It seems like $multi_edit_columns_name can also be of type null; however, parameter $multiEditColumnsName of PhpMyAdmin\Table::updateForeignKeys() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

275
                /** @scrutinizer ignore-type */ $multi_edit_columns_name,
Loading history...
276
                $_POST['destination_foreign_table'],
277
                $_POST['destination_foreign_column'],
278
                $options,
279
                $GLOBALS['table'],
280
                array_key_exists('foreign_keys_data', $relationsForeign)
281
                    ? $relationsForeign['foreign_keys_data']
282
                    : []
283
            );
284
            $this->response->addHTML($html);
285 8
        }
286
287 8
        // If there is a request for SQL previewing.
288 8
        if (isset($_POST['preview_sql'])) {
289
            Core::previewSQL($preview_sql_data);
290
291 8
            exit;
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...
292 4
        }
293
294 4
        if (empty($display_query) || $seen_error) {
295
            return;
296
        }
297 8
298 8
        $GLOBALS['display_query'] = $display_query;
299
        $this->response->addHTML(
300
            Generator::getMessage(
301 8
                __('Your SQL query has been executed successfully.'),
302
                null,
303
                'success'
304 8
            )
305 8
        );
306 8
    }
307
308
    /**
309
     * Update for internal relation
310
     *
311
     * @param array $relations Relations
312
     */
313
    private function updateForInternalRelation(
314
        Table $table,
315
        RelationFeature $relationFeature,
316
        array $relations
317 8
    ): void {
318
        $multi_edit_columns_name = $_POST['fields_name'] ?? null;
319 8
320 8
        if (
321
            ! $table->updateInternalRelations(
322 8
                $multi_edit_columns_name,
0 ignored issues
show
Bug introduced by
It seems like $multi_edit_columns_name can also be of type null; however, parameter $multiEditColumnsName of PhpMyAdmin\Table::updateInternalRelations() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

322
                /** @scrutinizer ignore-type */ $multi_edit_columns_name,
Loading history...
323 1
                $_POST['destination_db'],
324 4
                $_POST['destination_table'],
325 4
                $_POST['destination_column'],
326
                $relationFeature,
327 4
                $relations
328 4
            )
329
        ) {
330
            return;
331
        }
332 4
333
        $this->response->addHTML(
334
            Generator::getMessage(
335 1
                __('Internal relationships were successfully updated.'),
336 4
                '',
337 4
                'success'
338 4
            )
339
        );
340
    }
341 8
342 8
    /**
343
     * Send table columns for foreign table dropdown
344
     */
345 8
    public function getDropdownValueForTable(): void
346 2
    {
347
        $foreignTable = $_POST['foreignTable'];
348
        $table_obj = $this->dbi->getTable($_POST['foreignDb'], $foreignTable);
349
        // Since views do not have keys defined on them provide the full list of
350
        // columns
351
        if ($table_obj->isView()) {
352
            $columnList = $table_obj->getColumns(false, false);
353
        } else {
354
            $columnList = $table_obj->getIndexedColumns(false, false);
355
        }
356
357
        if ($GLOBALS['cfg']['NaturalOrder']) {
358
            usort($columnList, 'strnatcasecmp');
359
        }
360
361
        $this->response->addJSON('columns', $columnList);
362
363
        // @todo should be: $server->db($db)->table($table)->primary()
364
        $primary = Index::getPrimary($foreignTable, $_POST['foreignDb']);
365
        if ($primary === false) {
366
            return;
367
        }
368
369
        $this->response->addJSON('primary', array_keys($primary->getColumns()));
370
    }
371
372
    /**
373
     * Send database selection values for dropdown
374
     *
375
     * @param string $storageEngine Storage engine.
376
     */
377
    public function getDropdownValueForDatabase(string $storageEngine): void
378
    {
379
        $tables = [];
380
        $foreign = isset($_POST['foreign']) && $_POST['foreign'] === 'true';
381
382
        if ($foreign) {
383
            $query = 'SHOW TABLE STATUS FROM '
384
                . Util::backquote($_POST['foreignDb']);
385
            $tables_rs = $this->dbi->query($query);
386
387
            foreach ($tables_rs as $row) {
388
                if (! isset($row['Engine']) || mb_strtoupper($row['Engine']) != $storageEngine) {
389
                    continue;
390
                }
391
392
                $tables[] = $row['Name'];
393
            }
394
        } else {
395
            $query = 'SHOW TABLES FROM '
396
                . Util::backquote($_POST['foreignDb']);
397
            $tables_rs = $this->dbi->query($query);
398
            $tables = $tables_rs->fetchAllColumn();
399
        }
400
401
        if ($GLOBALS['cfg']['NaturalOrder']) {
402
            usort($tables, 'strnatcasecmp');
403
        }
404
405
        $this->response->addJSON('tables', $tables);
406
    }
407
}
408