CheckSchemaTask   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 304
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 40
eloc 143
dl 0
loc 304
rs 9.2
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getOptionParser() 0 21 1
C formatMessages() 0 55 12
A checkMigrationsStatus() 0 21 3
A main() 0 25 6
B checkDiff() 0 48 9
A checkSymbol() 0 10 2
A checkConventions() 0 41 5
A filterPhinxlogTables() 0 4 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2017 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
14
namespace BEdita\Core\Shell\Task;
15
16
use BEdita\Core\Model\Validation\SqlConventionsValidator;
17
use Cake\Console\ConsoleOptionParser;
18
use Cake\Console\Exception\MissingTaskException;
19
use Cake\Console\Shell;
20
use Cake\Core\Plugin;
21
use Cake\Database\Connection;
22
use Cake\Database\Driver\Mysql;
23
use Cake\Datasource\ConnectionManager;
24
use Cake\Utility\Hash;
25
use Cake\Utility\Inflector;
26
use Migrations\Migrations;
27
28
/**
29
 * Task to check if current schema is up to date, and if SQL standards are satisfied.
30
 *
31
 * @since 4.0.0
32
 */
33
class CheckSchemaTask extends Shell
34
{
35
    /**
36
     * Registry of all issues found.
37
     *
38
     * @var array
39
     */
40
    protected $messages = [];
41
42
    /**
43
     * List of SQL reserved words.
44
     *
45
     * @var array
46
     */
47
    protected $reservedWords = [];
48
49
    /**
50
     * {@inheritDoc}
51
     *
52
     * @codeCoverageIgnore
53
     */
54
    public function getOptionParser(): ConsoleOptionParser
55
    {
56
        $parser = parent::getOptionParser();
57
        $parser
58
            ->setDescription([
59
                'Current schema is compared with versioned schema dump to check if it is up to date.',
60
                'Also, migrations status and SQL naming conventions are checked.',
61
            ])
62
            ->addOption('connection', [
63
                'help' => 'Connection name to use.',
64
                'short' => 'c',
65
                'required' => false,
66
                'default' => 'default',
67
                'choices' => ConnectionManager::configured(),
68
            ])
69
            ->addOption('ignore-migration-status', [
70
                'help' => 'Skip checks on migration status.',
71
                'boolean' => true,
72
            ]);
73
74
        return $parser;
75
    }
76
77
    /**
78
     * Run checks on schema.
79
     *
80
     * @return bool
81
     */
82
    public function main()
83
    {
84
        if (!Plugin::isLoaded('Migrations')) {
85
            $this->abort('Plugin "Migrations" must be loaded in order to perform schema checks');
86
        }
87
88
        $connection = ConnectionManager::get($this->param('connection'));
89
        if (!($connection instanceof Connection)) {
90
            $this->abort('Unknown connection type');
91
        }
92
93
        if (!$this->param('ignore-migration-status')) {
94
            $this->checkMigrationsStatus($connection);
95
        }
96
97
        // check real vendor for DB like MariaDB or Aurora using MySQL driver where migration based check diff fails
98
        $realVendor = Hash::get((array)$connection->config(), 'realVendor');
99
        if (($connection->getDriver() instanceof Mysql) && empty($realVendor)) {
100
            $this->checkConventions($connection);
101
            $this->checkDiff($connection);
102
        } else {
103
            $this->out('=====> <warning>SQL conventions and schema differences can only be checked on MySQL</warning>');
104
        }
105
106
        return $this->formatMessages();
107
    }
108
109
    /**
110
     * Check if all migrations have already been migrated.
111
     *
112
     * @param \Cake\Database\Connection $connection Connection instance.
113
     * @return void
114
     */
115
    protected function checkMigrationsStatus(Connection $connection)
116
    {
117
        $migrations = new Migrations([
118
            'connection' => $connection->configName(),
119
            'plugin' => 'BEdita/Core',
120
        ]);
121
        $status = $migrations->status();
122
123
        $this->verbose('=====> Checking migrations status:');
124
        foreach ($status as $item) {
125
            $info = sprintf('=====>  - Migration <comment>%s</comment> (%s) is ', $item['name'], $item['id']);
126
            if ($item['status'] === 'up') {
127
                $this->verbose($info . '<info>UP</info>');
128
                continue;
129
            }
130
131
            $this->verbose($info . '<error>DOWN</error>');
132
            $this->messages['phinxlog'] = true;
133
        }
134
135
        $this->verbose('=====> ');
136
    }
137
138
    /**
139
     * Filter Phinxlog tables out of a list of table names.
140
     *
141
     * @param array $tables Table names.
142
     * @return array
143
     * @internal
144
     */
145
    protected function filterPhinxlogTables(array $tables)
146
    {
147
        return array_filter($tables, function ($table) {
148
            return $table !== 'phinxlog' && substr($table, -strlen('_phinxlog')) !== '_phinxlog';
149
        });
150
    }
151
152
    /**
153
     * Check if a symbol is valid.
154
     *
155
     * @param string $symbol Symbol to check.
156
     * @param array $context Index or constraint options.
157
     * @return array
158
     * @internal
159
     */
160
    protected function checkSymbol($symbol, array $context = [])
161
    {
162
        $validator = new SqlConventionsValidator();
163
        foreach ($context as $key => $value) {
164
            $validator->setProvider($key, $value);
165
        }
166
167
        $errors = $validator->validate(compact('symbol'));
168
169
        return Hash::get($errors, 'symbol', []);
170
    }
171
172
    /**
173
     * Check if SQL conventions are followed.
174
     *
175
     * @param \Cake\Database\Connection $connection Connection instance.
176
     * @return void
177
     */
178
    protected function checkConventions(Connection $connection)
179
    {
180
        $this->verbose('=====> Checking SQL conventions:');
181
        $allColumns = [];
182
        $tables = $this->filterPhinxlogTables($connection->getSchemaCollection()->listTables());
183
        foreach ($tables as $table) {
184
            $this->verbose(sprintf('=====>  - Checking table <comment>%s</comment>... ', $table), 0);
185
186
            $schema = $connection->getSchemaCollection()->describe($table);
187
            $errors = [];
188
189
            $errors['table']['naming'] = $this->checkSymbol($table);
190
191
            foreach ($schema->columns() as $column) {
192
                $errors['column'][$column]['naming'] = $this->checkSymbol(
193
                    $column,
194
                    compact('table', 'allColumns')
195
                );
196
                $allColumns[$column] = $table;
197
            }
198
199
            foreach ($schema->indexes() as $index) {
200
                $errors['index'][$index]['naming'] = $this->checkSymbol(
201
                    $index,
202
                    $schema->getIndex($index) + compact('table')
203
                );
204
            }
205
206
            foreach ($schema->constraints() as $constraint) {
207
                $errors['constraint'][$constraint]['naming'] = $this->checkSymbol(
208
                    $constraint,
209
                    $schema->getConstraint($constraint) + compact('table')
210
                );
211
            }
212
213
            $this->messages[$table] = $errors;
214
215
            $this->verbose('<info>DONE</info>');
216
        }
217
218
        $this->verbose('=====> ');
219
    }
220
221
    /**
222
     * Check if changes in schema occurred.
223
     *
224
     * @param \Cake\Database\Connection $connection Connection instance.
225
     * @return void
226
     */
227
    protected function checkDiff(Connection $connection)
228
    {
229
        try {
230
            $diffTask = $this->Tasks->load('Migrations.MigrationDiff');
231
        } catch (MissingTaskException $e) {
232
            $this->out(sprintf('=====> <error>Unable to check schema differences: %s</error>', $e->getMessage()));
233
234
            return;
235
        }
236
237
        $this->verbose('=====> Checking schema differences:');
238
239
        $diffTask->connection = $connection->configName();
240
        $diffTask->params['plugin'] = 'BEdita/Core';
241
        $diffTask->setup();
0 ignored issues
show
Bug introduced by
The method setup() does not exist on Cake\Console\Shell. It seems like you code against a sub-type of Cake\Console\Shell such as BEdita\Core\Shell\BeditaShell or Migrations\Shell\Task\MigrationDiffTask. ( Ignorable by Annotation )

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

241
        $diffTask->/** @scrutinizer ignore-call */ 
242
                   setup();
Loading history...
242
243
        $diff = $diffTask->templateData();
0 ignored issues
show
Bug introduced by
The method templateData() does not exist on Cake\Console\Shell. It seems like you code against a sub-type of Cake\Console\Shell such as Bake\Shell\Task\SimpleBakeTask. ( Ignorable by Annotation )

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

243
        /** @scrutinizer ignore-call */ 
244
        $diff = $diffTask->templateData();
Loading history...
244
        if (empty($diff['data'])) {
245
            return;
246
        }
247
        $diff = $diff['data'];
248
249
        $this->verbose('=====>  - Checking tables added or removed... ', 0);
250
        foreach ($this->filterPhinxlogTables(array_keys($diff['fullTables']['add'])) as $table) {
251
            $this->messages[$table]['table'][$table]['add'] = true;
252
        }
253
        foreach ($this->filterPhinxlogTables(array_keys($diff['fullTables']['remove'])) as $table) {
254
            $this->messages[$table]['table'][$table]['remove'] = true;
255
        }
256
        unset($diff['fullTables']);
257
        $this->verbose('<info>DONE</info>');
258
259
        foreach ($diff as $table => $elements) {
260
            $this->verbose(sprintf('=====>  - Checking table <comment>%s</comment>... ', $table), 0);
261
262
            foreach ($elements as $type => $changes) {
263
                $type = Inflector::singularize($type);
264
                foreach ($changes as $action => $list) {
265
                    foreach (array_keys($list) as $symbol) {
266
                        $this->messages[$table][$type][$symbol][$action] = true;
267
                    }
268
                }
269
            }
270
271
            $this->verbose('<info>DONE</info>');
272
        }
273
274
        $this->verbose('=====> ');
275
    }
276
277
    /**
278
     * Send all messages to output.
279
     *
280
     * @return bool
281
     */
282
    protected function formatMessages()
283
    {
284
        if (!empty($this->messages['phinxlog'])) {
285
            $this->quiet('=====> <warning>Migration history is not in sync with migration files.</warning>');
286
        }
287
        unset($this->messages['phinxlog']);
288
289
        ksort($this->messages);
290
291
        $check = true;
292
        foreach ($this->messages as $table => $elements) {
293
            $lines = [];
294
            foreach ($elements as $type => $list) {
295
                $type = Inflector::humanize($type);
296
                foreach ($list as $symbol => $messages) {
297
                    $messages = array_filter($messages);
298
                    foreach ($messages as $errorType => $details) {
299
                        switch ($errorType) {
300
                            case 'naming':
301
                                $lines[] = sprintf('%s name "%s" is not valid (%s)', $type, $symbol, implode(', ', $details));
302
                                break;
303
                            case 'add':
304
                                $lines[] = sprintf('%s "%s" has been added', $type, $symbol);
305
                                break;
306
                            case 'remove':
307
                                $lines[] = sprintf('%s "%s" has been removed', $type, $symbol);
308
                                break;
309
                            case 'changed':
310
                                $lines[] = sprintf('%s "%s" has been changed', $type, $symbol);
311
                                break;
312
                        }
313
                    }
314
                }
315
            }
316
317
            if (!empty($lines)) {
318
                $this->quiet(sprintf('=====> Table <comment>%s</comment>:', $table));
319
                $this->quiet(array_map(
320
                    function ($line) {
321
                        return sprintf('=====>  - <warning>%s</warning>', $line);
322
                    },
323
                    $lines
324
                ));
325
                $check = false;
326
            } else {
327
                $this->verbose(sprintf('=====> Table <comment>%s</comment>: <info>OK</info>', $table));
328
            }
329
        }
330
331
        if ($check) {
332
            $this->verbose('=====> ');
333
            $this->out('=====> <success>Everything seems just fine. Have a nice day!</success>');
334
        }
335
336
        return $check;
337
    }
338
}
339