Failed Conditions
Pull Request — master (#2850)
by Adrien
33:12 queued 23:11
created

SqliteSchemaManager   F

Complexity

Total Complexity 76

Size/Duplication

Total Lines 427
Duplicated Lines 0 %

Test Coverage

Coverage 84.98%

Importance

Changes 0
Metric Value
wmc 76
dl 0
loc 427
ccs 181
cts 213
cp 0.8498
rs 2.2388
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A dropForeignKey() 0 6 1
A dropDatabase() 0 4 2
C _getPortableTableColumnList() 0 52 16
A dropAndCreateForeignKey() 0 6 1
C _getPortableTableIndexesList() 0 47 8
A createForeignKey() 0 6 1
A renameTable() 0 6 1
A _getPortableTableIndexDefinition() 0 5 1
A createDatabase() 0 11 1
A _getPortableTableDefinition() 0 3 1
F _getPortableTableColumnDefinition() 0 69 15
A parseColumnCollationFromSQL() 0 10 2
C _getPortableTableForeignKeysList() 0 44 8
A parseColumnCommentFromSQL() 0 12 3
A _getPortableViewDefinition() 0 3 1
A getTableDiffForAlterForeignKey() 0 15 3
C listTableForeignKeys() 0 39 11

How to fix   Complexity   

Complex Class

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

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL\Schema;
21
22
use Doctrine\DBAL\DBALException;
23
use Doctrine\DBAL\Types\StringType;
24
use Doctrine\DBAL\Types\TextType;
25
use Doctrine\DBAL\Types\Type;
26
27
/**
28
 * Sqlite SchemaManager.
29
 *
30
 * @author Konsta Vesterinen <[email protected]>
31
 * @author Lukas Smith <[email protected]> (PEAR MDB2 library)
32
 * @author Jonathan H. Wage <[email protected]>
33
 * @author Martin Hasoň <[email protected]>
34
 * @since  2.0
35
 */
36
class SqliteSchemaManager extends AbstractSchemaManager
37
{
38
    /**
39
     * {@inheritdoc}
40
     */
41 2
    public function dropDatabase($database)
42
    {
43 2
        if (file_exists($database)) {
44 2
            unlink($database);
45
        }
46 2
    }
47
48
    /**
49
     * {@inheritdoc}
50
     */
51 2
    public function createDatabase($database)
52
    {
53 2
        $params = $this->_conn->getParams();
54 2
        $driver = $params['driver'];
55
        $options = [
56 2
            'driver' => $driver,
57 2
            'path' => $database
58
        ];
59 2
        $conn = \Doctrine\DBAL\DriverManager::getConnection($options);
60 2
        $conn->connect();
61 2
        $conn->close();
62 2
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67 1
    public function renameTable($name, $newName)
68
    {
69 1
        $tableDiff = new TableDiff($name);
70 1
        $tableDiff->fromTable = $this->listTableDetails($name);
71 1
        $tableDiff->newName = $newName;
72 1
        $this->alterTable($tableDiff);
73 1
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    public function createForeignKey(ForeignKeyConstraint $foreignKey, $table)
79
    {
80
        $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table);
81
        $tableDiff->addedForeignKeys[] = $foreignKey;
82
83
        $this->alterTable($tableDiff);
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table)
90
    {
91
        $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table);
92
        $tableDiff->changedForeignKeys[] = $foreignKey;
93
94
        $this->alterTable($tableDiff);
95
    }
96
97
    /**
98
     * {@inheritdoc}
99
     */
100
    public function dropForeignKey($foreignKey, $table)
101
    {
102
        $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table);
103
        $tableDiff->removedForeignKeys[] = $foreignKey;
104
105
        $this->alterTable($tableDiff);
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111 1
    public function listTableForeignKeys($table, $database = null)
112
    {
113 1
        if (null === $database) {
114 1
            $database = $this->_conn->getDatabase();
115
        }
116 1
        $sql = $this->_platform->getListTableForeignKeysSQL($table, $database);
0 ignored issues
show
Unused Code introduced by
The call to Doctrine\DBAL\Platforms\...stTableForeignKeysSQL() has too many arguments starting with $database. ( Ignorable by Annotation )

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

116
        /** @scrutinizer ignore-call */ 
117
        $sql = $this->_platform->getListTableForeignKeysSQL($table, $database);

This check compares calls to functions or methods with their respective definitions. If the call has more 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...
117 1
        $tableForeignKeys = $this->_conn->fetchAll($sql);
118
119 1
        if ( ! empty($tableForeignKeys)) {
120 1
            $createSql = $this->_conn->fetchAll("SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '$table'");
121 1
            $createSql = $createSql[0]['sql'] ?? '';
122
123 1
            if (preg_match_all('#
124
                    (?:CONSTRAINT\s+([^\s]+)\s+)?
125
                    (?:FOREIGN\s+KEY[^\)]+\)\s*)?
126
                    REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))?
127
                    (?:
128
                        [^,]*?
129
                        (NOT\s+DEFERRABLE|DEFERRABLE)
130
                        (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
131
                    )?#isx',
132 1
                    $createSql, $match)) {
133
134 1
                $names = array_reverse($match[1]);
135 1
                $deferrable = array_reverse($match[2]);
136 1
                $deferred = array_reverse($match[3]);
137
            } else {
138
                $names = $deferrable = $deferred = [];
139
            }
140
141 1
            foreach ($tableForeignKeys as $key => $value) {
142 1
                $id = $value['id'];
143 1
                $tableForeignKeys[$key]['constraint_name'] = isset($names[$id]) && '' != $names[$id] ? $names[$id] : $id;
144 1
                $tableForeignKeys[$key]['deferrable'] = isset($deferrable[$id]) && 'deferrable' == strtolower($deferrable[$id]) ? true : false;
145 1
                $tableForeignKeys[$key]['deferred'] = isset($deferred[$id]) && 'deferred' == strtolower($deferred[$id]) ? true : false;
146
            }
147
        }
148
149 1
        return $this->_getPortableTableForeignKeysList($tableForeignKeys);
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155 65
    protected function _getPortableTableDefinition($table)
156
    {
157 65
        return $table['name'];
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     *
163
     * @license New BSD License
164
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
165
     */
166 34
    protected function _getPortableTableIndexesList($tableIndexes, $tableName=null)
167
    {
168 34
        $indexBuffer = [];
169
170
        // fetch primary
171 34
        $stmt = $this->_conn->executeQuery("PRAGMA TABLE_INFO ('$tableName')");
172 34
        $indexArray = $stmt->fetchAll(\PDO::FETCH_ASSOC);
173
174 34
        usort($indexArray, function($a, $b) {
175 23
            if ($a['pk'] == $b['pk']) {
176 19
                return $a['cid'] - $b['cid'];
177
            }
178
179 12
            return $a['pk'] - $b['pk'];
180 34
        });
181 34
        foreach ($indexArray as $indexColumnRow) {
182 34
            if ($indexColumnRow['pk'] != "0") {
183 21
                $indexBuffer[] = [
184 21
                    'key_name' => 'primary',
185
                    'primary' => true,
186
                    'non_unique' => false,
187 34
                    'column_name' => $indexColumnRow['name']
188
                ];
189
            }
190
        }
191
192
        // fetch regular indexes
193 34
        foreach ($tableIndexes as $tableIndex) {
194
            // Ignore indexes with reserved names, e.g. autoindexes
195 8
            if (strpos($tableIndex['name'], 'sqlite_') !== 0) {
196 6
                $keyName = $tableIndex['name'];
197 6
                $idx = [];
198 6
                $idx['key_name'] = $keyName;
199 6
                $idx['primary'] = false;
200 6
                $idx['non_unique'] = $tableIndex['unique']?false:true;
201
202 6
                $stmt = $this->_conn->executeQuery("PRAGMA INDEX_INFO ('{$keyName}')");
203 6
                $indexArray = $stmt->fetchAll(\PDO::FETCH_ASSOC);
204
205 6
                foreach ($indexArray as $indexColumnRow) {
206 6
                    $idx['column_name'] = $indexColumnRow['name'];
207 8
                    $indexBuffer[] = $idx;
208
                }
209
            }
210
        }
211
212 34
        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218
    protected function _getPortableTableIndexDefinition($tableIndex)
219
    {
220
        return [
221
            'name' => $tableIndex['name'],
222
            'unique' => (bool) $tableIndex['unique']
223
        ];
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229 41
    protected function _getPortableTableColumnList($table, $database, $tableColumns)
230
    {
231 41
        $list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
232
233
        // find column with autoincrement
234 41
        $autoincrementColumn = null;
235 41
        $autoincrementCount = 0;
236
237 41
        foreach ($tableColumns as $tableColumn) {
238 41
            if ('0' != $tableColumn['pk']) {
239 24
                $autoincrementCount++;
240 24
                if (null === $autoincrementColumn && 'integer' == strtolower($tableColumn['type'])) {
241 41
                    $autoincrementColumn = $tableColumn['name'];
242
                }
243
            }
244
        }
245
246 41
        if (1 == $autoincrementCount && null !== $autoincrementColumn) {
247 23
            foreach ($list as $column) {
248 23
                if ($autoincrementColumn == $column->getName()) {
249 23
                    $column->setAutoincrement(true);
250
                }
251
            }
252
        }
253
254
        // inspect column collation and comments
255 41
        $createSql = $this->_conn->fetchAll("SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '$table'");
256 41
        $createSql = isset($createSql[0]['sql']) ? $createSql[0]['sql'] : '';
257
258 41
        foreach ($list as $columnName => $column) {
259 41
            $type = $column->getType();
260
261 41
            if ($type instanceof StringType || $type instanceof TextType) {
262 17
                $column->setPlatformOption('collation', $this->parseColumnCollationFromSQL($columnName, $createSql) ?: 'BINARY');
263
            }
264
265 41
            $comment = $this->parseColumnCommentFromSQL($columnName, $createSql);
266
267 41
            if ($comment !== null) {
268 16
                $type = $this->extractDoctrineTypeFromComment($comment, null);
269
270 16
                if (null !== $type) {
271 4
                    $column->setType(Type::getType($type));
272
273 4
                    $comment = $this->removeDoctrineTypeFromComment($comment, $type);
274
                }
275
276 41
                $column->setComment($comment);
277
            }
278
        }
279
280 41
        return $list;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286 41
    protected function _getPortableTableColumnDefinition($tableColumn)
287
    {
288 41
        $parts = explode('(', $tableColumn['type']);
289 41
        $tableColumn['type'] = trim($parts[0]);
290 41
        if (isset($parts[1])) {
291 13
            $length = trim($parts[1], ')');
292 13
            $tableColumn['length'] = $length;
293
        }
294
295 41
        $dbType = strtolower($tableColumn['type']);
296 41
        $length = isset($tableColumn['length']) ? $tableColumn['length'] : null;
297 41
        $unsigned = false;
298
299 41
        if (strpos($dbType, ' unsigned') !== false) {
300 1
            $dbType = str_replace(' unsigned', '', $dbType);
301 1
            $unsigned = true;
302
        }
303
304 41
        $fixed = false;
305 41
        $type = $this->_platform->getDoctrineTypeMapping($dbType);
306 41
        $default = $tableColumn['dflt_value'];
307 41
        if ($default == 'NULL') {
308 4
            $default = null;
309
        }
310 41
        if ($default !== null) {
311
            // SQLite returns strings wrapped in single quotes and escaped, so we need to strip them
312 7
            $default = preg_replace("/^'(.*)'$/s", '\1', $default);
313 7
            $default = str_replace("''", "'", $default);
314
        }
315 41
        $notnull = (bool) $tableColumn['notnull'];
316
317 41
        if ( ! isset($tableColumn['name'])) {
318
            $tableColumn['name'] = '';
319
        }
320
321 41
        $precision = null;
322 41
        $scale = null;
323
324
        switch ($dbType) {
325 41
            case 'char':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
326 4
                $fixed = true;
327 4
                break;
328 40
            case 'float':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
329 40
            case 'double':
330 40
            case 'real':
331 40
            case 'decimal':
332 40
            case 'numeric':
333 4
                if (isset($tableColumn['length'])) {
334 4
                    if (strpos($tableColumn['length'], ',') === false) {
335
                        $tableColumn['length'] .= ",0";
336
                    }
337 4
                    list($precision, $scale) = array_map('trim', explode(',', $tableColumn['length']));
338
                }
339 4
                $length = null;
340 4
                break;
341
        }
342
343
        $options = [
344 41
            'length'   => $length,
345 41
            'unsigned' => (bool) $unsigned,
346 41
            'fixed'    => $fixed,
347 41
            'notnull'  => $notnull,
348 41
            'default'  => $default,
349 41
            'precision' => $precision,
350 41
            'scale'     => $scale,
351
            'autoincrement' => false,
352
        ];
353
354 41
        return new Column($tableColumn['name'], \Doctrine\DBAL\Types\Type::getType($type), $options);
355
    }
356
357
    /**
358
     * {@inheritdoc}
359
     */
360 1
    protected function _getPortableViewDefinition($view)
361
    {
362 1
        return new View($view['name'], $view['sql']);
363
    }
364
365
    /**
366
     * {@inheritdoc}
367
     */
368 1
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
369
    {
370 1
        $list = [];
371 1
        foreach ($tableForeignKeys as $value) {
372 1
            $value = array_change_key_case($value, CASE_LOWER);
373 1
            $name = $value['constraint_name'];
374 1
            if ( ! isset($list[$name])) {
375 1
                if ( ! isset($value['on_delete']) || $value['on_delete'] == "RESTRICT") {
376
                    $value['on_delete'] = null;
377
                }
378 1
                if ( ! isset($value['on_update']) || $value['on_update'] == "RESTRICT") {
379
                    $value['on_update'] = null;
380
                }
381
382 1
                $list[$name] = [
383 1
                    'name' => $name,
384
                    'local' => [],
385
                    'foreign' => [],
386 1
                    'foreignTable' => $value['table'],
387 1
                    'onDelete' => $value['on_delete'],
388 1
                    'onUpdate' => $value['on_update'],
389 1
                    'deferrable' => $value['deferrable'],
390 1
                    'deferred'=> $value['deferred'],
391
                ];
392
            }
393 1
            $list[$name]['local'][] = $value['from'];
394 1
            $list[$name]['foreign'][] = $value['to'];
395
        }
396
397 1
        $result = [];
398 1
        foreach ($list as $constraint) {
399 1
            $result[] = new ForeignKeyConstraint(
400 1
                array_values($constraint['local']), $constraint['foreignTable'],
401 1
                array_values($constraint['foreign']), $constraint['name'],
402
                [
403 1
                    'onDelete' => $constraint['onDelete'],
404 1
                    'onUpdate' => $constraint['onUpdate'],
405 1
                    'deferrable' => $constraint['deferrable'],
406 1
                    'deferred'=> $constraint['deferred'],
407
                ]
408
            );
409
        }
410
411 1
        return $result;
412
    }
413
414
    /**
415
     * @param \Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey
416
     * @param \Doctrine\DBAL\Schema\Table|string         $table
417
     *
418
     * @return \Doctrine\DBAL\Schema\TableDiff
419
     *
420
     * @throws \Doctrine\DBAL\DBALException
421
     */
422
    private function getTableDiffForAlterForeignKey(ForeignKeyConstraint $foreignKey, $table)
0 ignored issues
show
Unused Code introduced by
The parameter $foreignKey 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

422
    private function getTableDiffForAlterForeignKey(/** @scrutinizer ignore-unused */ ForeignKeyConstraint $foreignKey, $table)

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...
423
    {
424
        if ( ! $table instanceof Table) {
425
            $tableDetails = $this->tryMethod('listTableDetails', $table);
426
            if (false === $table) {
427
                throw new DBALException(sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table));
428
            }
429
430
            $table = $tableDetails;
431
        }
432
433
        $tableDiff = new TableDiff($table->getName());
434
        $tableDiff->fromTable = $table;
0 ignored issues
show
Documentation Bug introduced by
It seems like $table can also be of type false. However, the property $fromTable is declared as type Doctrine\DBAL\Schema\Table. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
435
436
        return $tableDiff;
437
    }
438
439 25
    private function parseColumnCollationFromSQL(string $column, string $sql) : ?string
440
    {
441 25
        $pattern = '{(?:\W' . preg_quote($column) . '\W|\W' . preg_quote($this->_platform->quoteSingleIdentifier($column))
442 25
                 . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}isx';
443
444 25
        if (preg_match($pattern, $sql, $match) !== 1) {
445 18
            return null;
446
        }
447
448 10
        return $match[1];
449
    }
450
451 50
    private function parseColumnCommentFromSQL(string $column, string $sql) : ?string
452
    {
453 50
        $pattern = '{[\s(,](?:\W' . preg_quote($this->_platform->quoteSingleIdentifier($column)) . '\W|\W' . preg_quote($column)
454 50
                 . '\W)(?:\(.*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}ix';
455
456 50
        if (preg_match($pattern, $sql, $match) !== 1) {
457 41
            return null;
458
        }
459
460 20
        $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
461
462 20
        return '' === $comment ? null : $comment;
463
    }
464
}
465