PostgreSqlSchemaManager   F
last analyzed

Complexity

Total Complexity 82

Size/Duplication

Total Lines 446
Duplicated Lines 5.61 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
dl 25
loc 446
rs 2
c 0
b 0
f 0
wmc 82
lcom 1
cbo 11

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getSchemaNames() 0 6 1
A getSchemaSearchPaths() 0 11 2
A getExistingSchemaSearchPaths() 0 8 2
A determineExistingSchemaSearchPaths() 0 9 1
A dropDatabase() 0 25 3
A _getPortableTableForeignKeyDefinition() 0 28 4
A _getPortableTriggerDefinition() 0 4 1
A _getPortableViewDefinition() 0 4 1
A _getPortableUserDefinition() 0 7 1
A _getPortableTableDefinition() 0 11 2
A _getPortableTableIndexesList() 0 30 5
A _getPortableDatabaseDefinition() 0 4 1
A _getPortableSequencesList() 5 22 4
A getPortableNamespaceDefinition() 0 4 1
A _getPortableSequenceDefinition() 5 17 3
F _getPortableTableColumnDefinition() 15 152 48
A fixVersion94NegativeNumericDefaultValue() 0 8 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like PostgreSqlSchemaManager 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 PostgreSqlSchemaManager, 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\Exception\DriverException;
23
use Doctrine\DBAL\FetchMode;
24
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
25
use Doctrine\DBAL\Types\Type;
26
use const CASE_LOWER;
27
use function array_change_key_case;
28
use function array_filter;
29
use function array_keys;
30
use function array_map;
31
use function array_shift;
32
use function assert;
33
use function explode;
34
use function in_array;
35
use function join;
36
use function preg_match;
37
use function preg_replace;
38
use function str_replace;
39
use function stripos;
40
use function strlen;
41
use function strpos;
42
use function strtolower;
43
use function trim;
44
45
/**
46
 * PostgreSQL Schema Manager.
47
 *
48
 * @author Konsta Vesterinen <[email protected]>
49
 * @author Lukas Smith <[email protected]> (PEAR MDB2 library)
50
 * @author Benjamin Eberlei <[email protected]>
51
 * @since  2.0
52
 */
53
class PostgreSqlSchemaManager extends AbstractSchemaManager
54
{
55
    /**
56
     * @var array
57
     */
58
    private $existingSchemaPaths;
59
60
    /**
61
     * Gets all the existing schema names.
62
     *
63
     * @return array
64
     */
65
    public function getSchemaNames()
66
    {
67
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
68
69
        return $statement->fetchAll(FetchMode::COLUMN);
70
    }
71
72
    /**
73
     * Returns an array of schema search paths.
74
     *
75
     * This is a PostgreSQL only function.
76
     *
77
     * @return array
78
     */
79
    public function getSchemaSearchPaths()
80
    {
81
        $params = $this->_conn->getParams();
82
        $schema = explode(",", $this->_conn->fetchColumn('SHOW search_path'));
83
84
        if (isset($params['user'])) {
85
            $schema = str_replace('"$user"', $params['user'], $schema);
86
        }
87
88
        return array_map('trim', $schema);
89
    }
90
91
    /**
92
     * Gets names of all existing schemas in the current users search path.
93
     *
94
     * This is a PostgreSQL only function.
95
     *
96
     * @return array
97
     */
98
    public function getExistingSchemaSearchPaths()
99
    {
100
        if ($this->existingSchemaPaths === null) {
101
            $this->determineExistingSchemaSearchPaths();
102
        }
103
104
        return $this->existingSchemaPaths;
105
    }
106
107
    /**
108
     * Sets or resets the order of the existing schemas in the current search path of the user.
109
     *
110
     * This is a PostgreSQL only function.
111
     *
112
     * @return void
113
     */
114
    public function determineExistingSchemaSearchPaths()
115
    {
116
        $names = $this->getSchemaNames();
117
        $paths = $this->getSchemaSearchPaths();
118
119
        $this->existingSchemaPaths = array_filter($paths, function ($v) use ($names) {
120
            return in_array($v, $names);
121
        });
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127
    public function dropDatabase($database)
128
    {
129
        try {
130
            parent::dropDatabase($database);
131
        } catch (DriverException $exception) {
132
            // If we have a SQLSTATE 55006, the drop database operation failed
133
            // because of active connections on the database.
134
            // To force dropping the database, we first have to close all active connections
135
            // on that database and issue the drop database operation again.
136
            if ($exception->getSQLState() !== '55006') {
137
                throw $exception;
138
            }
139
140
            assert($this->_platform instanceof PostgreSqlPlatform);
141
142
            $this->_execSql(
143
                [
144
                    $this->_platform->getDisallowDatabaseConnectionsSQL($database),
145
                    $this->_platform->getCloseActiveDatabaseConnectionsSQL($database),
146
                ]
147
            );
148
149
            parent::dropDatabase($database);
150
        }
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
157
    {
158
        $onUpdate       = null;
159
        $onDelete       = null;
160
        $localColumns   = null;
161
        $foreignColumns = null;
162
        $foreignTable   = null;
163
164
        if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
165
            $onUpdate = $match[1];
166
        }
167
        if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
168
            $onDelete = $match[1];
169
        }
170
171
        if (preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values)) {
172
            // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
173
            // the idea to trim them here.
174
            $localColumns = array_map('trim', explode(",", $values[1]));
175
            $foreignColumns = array_map('trim', explode(",", $values[3]));
176
            $foreignTable = $values[2];
177
        }
178
179
        return new ForeignKeyConstraint(
180
            $localColumns, $foreignTable, $foreignColumns, $tableForeignKey['conname'],
0 ignored issues
show
Bug introduced by
It seems like $localColumns defined by null on line 160 can also be of type null; however, Doctrine\DBAL\Schema\For...nstraint::__construct() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $foreignColumns defined by null on line 161 can also be of type null; however, Doctrine\DBAL\Schema\For...nstraint::__construct() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
181
            ['onUpdate' => $onUpdate, 'onDelete' => $onDelete]
182
        );
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188
    protected function _getPortableTriggerDefinition($trigger)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
189
    {
190
        return $trigger['trigger_name'];
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196
    protected function _getPortableViewDefinition($view)
197
    {
198
        return new View($view['schemaname'].'.'.$view['viewname'], $view['definition']);
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204
    protected function _getPortableUserDefinition($user)
205
    {
206
        return [
207
            'user' => $user['usename'],
208
            'password' => $user['passwd']
209
        ];
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     */
215
    protected function _getPortableTableDefinition($table)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
216
    {
217
        $schemas = $this->getExistingSchemaSearchPaths();
218
        $firstSchema = array_shift($schemas);
219
220
        if ($table['schema_name'] == $firstSchema) {
221
            return $table['table_name'];
222
        }
223
224
        return $table['schema_name'] . "." . $table['table_name'];
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     *
230
     * @license New BSD License
231
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
232
     */
233
    protected function _getPortableTableIndexesList($tableIndexes, $tableName=null)
234
    {
235
        $buffer = [];
236
        foreach ($tableIndexes as $row) {
237
            $colNumbers = explode(' ', $row['indkey']);
238
            $colNumbersSql = 'IN (' . join(' ,', $colNumbers) . ' )';
239
            $columnNameSql = "SELECT attnum, attname FROM pg_attribute
240
                WHERE attrelid={$row['indrelid']} AND attnum $colNumbersSql ORDER BY attnum ASC;";
241
242
            $stmt = $this->_conn->executeQuery($columnNameSql);
243
            $indexColumns = $stmt->fetchAll();
244
245
            // required for getting the order of the columns right.
246
            foreach ($colNumbers as $colNum) {
247
                foreach ($indexColumns as $colRow) {
248
                    if ($colNum == $colRow['attnum']) {
249
                        $buffer[] = [
250
                            'key_name' => $row['relname'],
251
                            'column_name' => trim($colRow['attname']),
252
                            'non_unique' => !$row['indisunique'],
253
                            'primary' => $row['indisprimary'],
254
                            'where' => $row['where'],
255
                        ];
256
                    }
257
                }
258
            }
259
        }
260
261
        return parent::_getPortableTableIndexesList($buffer, $tableName);
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267
    protected function _getPortableDatabaseDefinition($database)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
268
    {
269
        return $database['datname'];
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    protected function _getPortableSequencesList($sequences)
276
    {
277
        $sequenceDefinitions = [];
278
279
        foreach ($sequences as $sequence) {
280 View Code Duplication
            if ($sequence['schemaname'] != 'public') {
281
                $sequenceName = $sequence['schemaname'] . "." . $sequence['relname'];
282
            } else {
283
                $sequenceName = $sequence['relname'];
284
            }
285
286
            $sequenceDefinitions[$sequenceName] = $sequence;
287
        }
288
289
        $list = [];
290
291
        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
292
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
293
        }
294
295
        return $list;
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301
    protected function getPortableNamespaceDefinition(array $namespace)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
302
    {
303
        return $namespace['nspname'];
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309
    protected function _getPortableSequenceDefinition($sequence)
310
    {
311 View Code Duplication
        if ($sequence['schemaname'] !== 'public') {
312
            $sequenceName = $sequence['schemaname'] . "." . $sequence['relname'];
313
        } else {
314
            $sequenceName = $sequence['relname'];
315
        }
316
317
        if ( ! isset($sequence['increment_by'], $sequence['min_value'])) {
318
            /** @var string[] $data */
319
            $data      = $this->_conn->fetchAssoc('SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName));
320
321
            $sequence += $data;
322
        }
323
324
        return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
325
    }
326
327
    /**
328
     * {@inheritdoc}
329
     */
330
    protected function _getPortableTableColumnDefinition($tableColumn)
331
    {
332
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
333
334
        if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
335
            // get length from varchar definition
336
            $length = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']);
337
            $tableColumn['length'] = $length;
338
        }
339
340
        $matches = [];
341
342
        $autoincrement = false;
343
        if (preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) {
344
            $tableColumn['sequence'] = $matches[1];
345
            $tableColumn['default'] = null;
346
            $autoincrement = true;
347
        }
348
349
        if (preg_match("/^['(](.*)[')]::.*$/", $tableColumn['default'], $matches)) {
350
            $tableColumn['default'] = $matches[1];
351
        }
352
353
        if (stripos($tableColumn['default'], 'NULL') === 0) {
354
            $tableColumn['default'] = null;
355
        }
356
357
        $length = $tableColumn['length'] ?? null;
358
        if ($length == '-1' && isset($tableColumn['atttypmod'])) {
359
            $length = $tableColumn['atttypmod'] - 4;
360
        }
361
        if ((int) $length <= 0) {
362
            $length = null;
363
        }
364
        $fixed = null;
365
366
        if (!isset($tableColumn['name'])) {
367
            $tableColumn['name'] = '';
368
        }
369
370
        $precision = null;
371
        $scale = null;
372
        $jsonb = null;
373
374
        $dbType = strtolower($tableColumn['type']);
375
        if (strlen($tableColumn['domain_type']) && !$this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])) {
376
            $dbType = strtolower($tableColumn['domain_type']);
377
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
378
        }
379
380
        $type = $this->_platform->getDoctrineTypeMapping($dbType);
381
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
382
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
383
384
        switch ($dbType) {
385
            case 'smallint':
386 View Code Duplication
            case 'int2':
387
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
388
                $length = null;
389
                break;
390
            case 'int':
391
            case 'int4':
392 View Code Duplication
            case 'integer':
393
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
394
                $length = null;
395
                break;
396
            case 'bigint':
397 View Code Duplication
            case 'int8':
398
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
399
                $length = null;
400
                break;
401
            case 'bool':
402
            case 'boolean':
403
                if ($tableColumn['default'] === 'true') {
404
                    $tableColumn['default'] = true;
405
                }
406
407
                if ($tableColumn['default'] === 'false') {
408
                    $tableColumn['default'] = false;
409
                }
410
411
                $length = null;
412
                break;
413
            case 'text':
414
                $fixed = false;
415
                break;
416
            case 'varchar':
417
            case 'interval':
418
            case '_varchar':
419
                $fixed = false;
420
                break;
421
            case 'char':
422
            case 'bpchar':
423
                $fixed = true;
424
                break;
425
            case 'float':
426
            case 'float4':
427
            case 'float8':
428
            case 'double':
429
            case 'double precision':
430
            case 'real':
431
            case 'decimal':
432
            case 'money':
433
            case 'numeric':
434
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
435
436
                if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) {
437
                    $precision = $match[1];
438
                    $scale = $match[2];
439
                    $length = null;
440
                }
441
                break;
442
            case 'year':
443
                $length = null;
444
                break;
445
446
            // PostgreSQL 9.4+ only
447
            case 'jsonb':
448
                $jsonb = true;
449
                break;
450
        }
451
452
        if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) {
453
            $tableColumn['default'] = $match[1];
454
        }
455
456
        $options = [
457
            'length'        => $length,
458
            'notnull'       => (bool) $tableColumn['isnotnull'],
459
            'default'       => $tableColumn['default'],
460
            'precision'     => $precision,
461
            'scale'         => $scale,
462
            'fixed'         => $fixed,
463
            'unsigned'      => false,
464
            'autoincrement' => $autoincrement,
465
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
466
                ? $tableColumn['comment']
467
                : null,
468
        ];
469
470
        $column = new Column($tableColumn['field'], Type::getType($type), $options);
471
472 View Code Duplication
        if (isset($tableColumn['collation']) && !empty($tableColumn['collation'])) {
473
            $column->setPlatformOption('collation', $tableColumn['collation']);
474
        }
475
476
        if (in_array($column->getType()->getName(), [Type::JSON_ARRAY, Type::JSON], true)) {
477
            $column->setPlatformOption('jsonb', $jsonb);
478
        }
479
480
        return $column;
481
    }
482
483
    /**
484
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
485
     *
486
     * @param mixed $defaultValue
487
     *
488
     * @return mixed
489
     */
490
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
491
    {
492
        if (strpos($defaultValue, '(') === 0) {
493
            return trim($defaultValue, '()');
494
        }
495
496
        return $defaultValue;
497
    }
498
}
499