Failed Conditions
Push — master ( fa7802...d60694 )
by Guilherme
09:27
created

DatabaseDriver   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 500
Duplicated Lines 0 %

Test Coverage

Coverage 54.07%

Importance

Changes 0
Metric Value
eloc 206
c 0
b 0
f 0
dl 0
loc 500
ccs 113
cts 209
cp 0.5407
rs 2.56
wmc 73

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
B buildFieldMappings() 0 40 9
A isTransient() 0 3 1
A setNamespace() 0 3 1
B loadMetadataForClass() 0 90 11
C convertColumnAnnotationToFieldMetadata() 0 54 17
A getAllClassNames() 0 5 1
A setFieldNameForColumn() 0 3 1
A getFieldNameForColumn() 0 14 3
A getTableForeignKeys() 0 5 2
A setTables() 0 13 3
A getTablePrimaryKeys() 0 9 2
B reverseEngineerMappingFromDatabase() 0 46 9
A buildTable() 0 24 3
A setClassNameForTable() 0 3 1
A getClassNameForTable() 0 5 1
B buildToOneAssociationMappings() 0 38 7

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Mapping\Driver;
6
7
use Doctrine\Common\Inflector\Inflector;
8
use Doctrine\DBAL\Schema\AbstractSchemaManager;
9
use Doctrine\DBAL\Schema\Column;
10
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
11
use Doctrine\DBAL\Schema\Identifier;
12
use Doctrine\DBAL\Schema\Index;
13
use Doctrine\DBAL\Schema\SchemaException;
14
use Doctrine\DBAL\Schema\Table;
15
use Doctrine\DBAL\Types\Type;
16
use Doctrine\ORM\Mapping;
17
use Doctrine\ORM\Sequencing\Generator;
18
use InvalidArgumentException;
19
use function array_diff;
20
use function array_keys;
21
use function array_merge;
22
use function count;
23
use function current;
24
use function in_array;
25
use function sort;
26
use function str_replace;
27
use function strtolower;
28
29
/**
30
 * The DatabaseDriver reverse engineers the mapping metadata from a database.
31
 */
32
class DatabaseDriver implements MappingDriver
33
{
34
    /** @var AbstractSchemaManager */
35
    private $sm;
36
37
    /** @var Table[]|null */
38
    private $tables;
39
40
    /** @var string[] */
41
    private $classToTableNames = [];
42
43
    /** @var Table[] */
44
    private $manyToManyTables = [];
45
46
    /** @var Table[] */
47
    private $classNamesForTables = [];
48
49
    /** @var Table[][] */
50
    private $fieldNamesForColumns = [];
51
52
    /**
53
     * The namespace for the generated entities.
54
     *
55
     * @var string|null
56
     */
57
    private $namespace;
58
59 2
    public function __construct(AbstractSchemaManager $schemaManager)
60
    {
61 2
        $this->sm = $schemaManager;
62 2
    }
63
64
    /**
65
     * Set the namespace for the generated entities.
66
     *
67
     * @param string $namespace
68
     */
69
    public function setNamespace($namespace)
70
    {
71
        $this->namespace = $namespace;
72
    }
73
74
    /**
75
     * {@inheritDoc}
76
     */
77
    public function isTransient($className) : bool
78
    {
79
        return true;
80
    }
81
82
    /**
83
     * {@inheritDoc}
84
     */
85 2
    public function getAllClassNames() : array
86
    {
87 2
        $this->reverseEngineerMappingFromDatabase();
88
89 2
        return array_keys($this->classToTableNames);
90
    }
91
92
    /**
93
     * Sets class name for a table.
94
     *
95
     * @param string $tableName
96
     * @param string $className
97
     */
98
    public function setClassNameForTable($tableName, $className)
99
    {
100
        $this->classNamesForTables[$tableName] = $className;
101
    }
102
103
    /**
104
     * Sets field name for a column on a specific table.
105
     *
106
     * @param string $tableName
107
     * @param string $columnName
108
     * @param string $fieldName
109
     */
110
    public function setFieldNameForColumn($tableName, $columnName, $fieldName)
111
    {
112
        $this->fieldNamesForColumns[$tableName][$columnName] = $fieldName;
113
    }
114
115
    /**
116
     * Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager.
117
     *
118
     * @param Table[] $entityTables
119
     * @param Table[] $manyToManyTables
120
     */
121 2
    public function setTables($entityTables, $manyToManyTables)
122
    {
123 2
        $this->tables = $this->manyToManyTables = $this->classToTableNames = [];
124
125 2
        foreach ($entityTables as $table) {
126 2
            $className = $this->getClassNameForTable($table->getName());
127
128 2
            $this->classToTableNames[$className] = $table->getName();
129 2
            $this->tables[$table->getName()]     = $table;
130
        }
131
132 2
        foreach ($manyToManyTables as $table) {
133 1
            $this->manyToManyTables[$table->getName()] = $table;
134
        }
135 2
    }
136
137
    /**
138
     * {@inheritDoc}
139
     */
140 2
    public function loadMetadataForClass(
141
        string $className,
142
        ?Mapping\ComponentMetadata $parent,
143
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
144
    ) : Mapping\ComponentMetadata {
145 2
        $this->reverseEngineerMappingFromDatabase();
146
147 2
        if (! isset($this->classToTableNames[$className])) {
148
            throw new InvalidArgumentException('Unknown class ' . $className);
149
        }
150
151 2
        $metadata = new Mapping\ClassMetadata($className, $parent, $metadataBuildingContext);
152
153 2
        $this->buildTable($metadata);
154 2
        $this->buildFieldMappings($metadata);
155 2
        $this->buildToOneAssociationMappings($metadata);
156
157 2
        $loweredTableName = strtolower($metadata->getTableName());
158
159 2
        foreach ($this->manyToManyTables as $manyTable) {
160 1
            foreach ($manyTable->getForeignKeys() as $foreignKey) {
161
                // foreign key maps to the table of the current entity, many to many association probably exists
162 1
                if ($loweredTableName !== strtolower($foreignKey->getForeignTableName())) {
163 1
                    continue;
164
                }
165
166 1
                $myFk    = $foreignKey;
167 1
                $otherFk = null;
168
169 1
                foreach ($manyTable->getForeignKeys() as $manyTableForeignKey) {
170 1
                    if ($manyTableForeignKey !== $myFk) {
171
                        $otherFk = $manyTableForeignKey;
172
173
                        break;
174
                    }
175
                }
176
177 1
                if (! $otherFk) {
178
                    // the definition of this many to many table does not contain
179
                    // enough foreign key information to continue reverse engineering.
180 1
                    continue;
181
                }
182
183
                $localColumn = current($myFk->getColumns());
184
185
                $associationMapping                 = [];
186
                $associationMapping['fieldName']    = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getColumns()), true);
187
                $associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName());
188
189
                if (current($manyTable->getColumns())->getName() === $localColumn) {
190
                    $associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true);
191
                    $associationMapping['joinTable']  = new Mapping\JoinTableMetadata();
192
193
                    $joinTable = $associationMapping['joinTable'];
194
                    $joinTable->setName(strtolower($manyTable->getName()));
195
196
                    $fkCols = $myFk->getForeignColumns();
197
                    $cols   = $myFk->getColumns();
198
199
                    for ($i = 0, $l = count($cols); $i < $l; $i++) {
200
                        $joinColumn = new Mapping\JoinColumnMetadata();
201
202
                        $joinColumn->setColumnName($cols[$i]);
203
                        $joinColumn->setReferencedColumnName($fkCols[$i]);
204
205
                        $joinTable->addJoinColumn($joinColumn);
206
                    }
207
208
                    $fkCols = $otherFk->getForeignColumns();
209
                    $cols   = $otherFk->getColumns();
210
211
                    for ($i = 0, $l = count($cols); $i < $l; $i++) {
212
                        $joinColumn = new Mapping\JoinColumnMetadata();
213
214
                        $joinColumn->setColumnName($cols[$i]);
215
                        $joinColumn->setReferencedColumnName($fkCols[$i]);
216
217
                        $joinTable->addInverseJoinColumn($joinColumn);
218
                    }
219
                } else {
220
                    $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true);
221
                }
222
223
                $metadata->addProperty($associationMapping);
0 ignored issues
show
Bug introduced by
$associationMapping of type array is incompatible with the type Doctrine\ORM\Mapping\Property expected by parameter $property of Doctrine\ORM\Mapping\ClassMetadata::addProperty(). ( Ignorable by Annotation )

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

223
                $metadata->addProperty(/** @scrutinizer ignore-type */ $associationMapping);
Loading history...
224
225
                break;
226
            }
227
        }
228
229 2
        return $metadata;
230
    }
231
232
    /**
233
     * @throws Mapping\MappingException
234
     */
235 2
    private function reverseEngineerMappingFromDatabase()
236
    {
237 2
        if ($this->tables !== null) {
238 2
            return;
239
        }
240
241
        $tables = [];
242
243
        foreach ($this->sm->listTableNames() as $tableName) {
244
            $tables[$tableName] = $this->sm->listTableDetails($tableName);
245
        }
246
247
        $this->tables = $this->manyToManyTables = $this->classToTableNames = [];
248
249
        foreach ($tables as $tableName => $table) {
250
            $foreignKeys = $this->sm->getDatabasePlatform()->supportsForeignKeyConstraints()
251
                ? $table->getForeignKeys()
252
                : [];
253
254
            $allForeignKeyColumns = [];
255
256
            foreach ($foreignKeys as $foreignKey) {
257
                $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns());
258
            }
259
260
            if (! $table->hasPrimaryKey()) {
261
                throw new Mapping\MappingException(
262
                    'Table ' . $table->getName() . ' has no primary key. Doctrine does not ' .
263
                    "support reverse engineering from tables that don't have a primary key."
264
                );
265
            }
266
267
            $pkColumns = $table->getPrimaryKey()->getColumns();
268
269
            sort($pkColumns);
270
            sort($allForeignKeyColumns);
271
272
            if ($pkColumns === $allForeignKeyColumns && count($foreignKeys) === 2) {
273
                $this->manyToManyTables[$tableName] = $table;
274
            } else {
275
                // lower-casing is necessary because of Oracle Uppercase Tablenames,
276
                // assumption is lower-case + underscore separated.
277
                $className = $this->getClassNameForTable($tableName);
278
279
                $this->tables[$tableName]            = $table;
280
                $this->classToTableNames[$className] = $tableName;
281
            }
282
        }
283
    }
284
285
    /**
286
     * Build table from a class metadata.
287
     */
288 2
    private function buildTable(Mapping\ClassMetadata $metadata)
289
    {
290 2
        $tableName     = $this->classToTableNames[$metadata->getClassName()];
291 2
        $indexes       = $this->tables[$tableName]->getIndexes();
292 2
        $tableMetadata = new Mapping\TableMetadata();
293
294 2
        $tableMetadata->setName($this->classToTableNames[$metadata->getClassName()]);
295
296 2
        foreach ($indexes as $index) {
297
            /** @var Index $index */
298 2
            if ($index->isPrimary()) {
299 2
                continue;
300
            }
301
302 1
            $tableMetadata->addIndex([
303 1
                'name'    => $index->getName(),
304 1
                'columns' => $index->getColumns(),
305 1
                'unique'  => $index->isUnique(),
306 1
                'options' => $index->getOptions(),
307 1
                'flags'   => $index->getFlags(),
308
            ]);
309
        }
310
311 2
        $metadata->setTable($tableMetadata);
312 2
    }
313
314
    /**
315
     * Build field mapping from class metadata.
316
     */
317 2
    private function buildFieldMappings(Mapping\ClassMetadata $metadata)
318
    {
319 2
        $tableName      = $metadata->getTableName();
320 2
        $columns        = $this->tables[$tableName]->getColumns();
321 2
        $primaryKeys    = $this->getTablePrimaryKeys($this->tables[$tableName]);
322 2
        $foreignKeys    = $this->getTableForeignKeys($this->tables[$tableName]);
323 2
        $allForeignKeys = [];
324
325 2
        foreach ($foreignKeys as $foreignKey) {
326
            $allForeignKeys = array_merge($allForeignKeys, $foreignKey->getLocalColumns());
327
        }
328
329 2
        $ids = [];
330
331 2
        foreach ($columns as $column) {
332 2
            if (in_array($column->getName(), $allForeignKeys, true)) {
333
                continue;
334
            }
335
336 2
            $fieldName     = $this->getFieldNameForColumn($tableName, $column->getName(), false);
337 2
            $fieldMetadata = $this->convertColumnAnnotationToFieldMetadata($tableName, $column, $fieldName);
0 ignored issues
show
Bug introduced by
It seems like $tableName can also be of type null; however, parameter $tableName of Doctrine\ORM\Mapping\Dri...tationToFieldMetadata() does only seem to accept string, 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

337
            $fieldMetadata = $this->convertColumnAnnotationToFieldMetadata(/** @scrutinizer ignore-type */ $tableName, $column, $fieldName);
Loading history...
338
339 2
            if ($primaryKeys && in_array($column->getName(), $primaryKeys, true)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $primaryKeys of type Doctrine\DBAL\Schema\Identifier[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
340 2
                $fieldMetadata->setPrimaryKey(true);
341
342 2
                $ids[] = $fieldMetadata;
343
            }
344
345 2
            $metadata->addProperty($fieldMetadata);
346
        }
347
348
        // We need to check for the columns here, because we might have associations as id as well.
349 2
        if ($ids && count($primaryKeys) === 1) {
350 2
            $generator = $fieldMetadata->getTypeName() === 'bigint'
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fieldMetadata does not seem to be defined for all execution paths leading up to this point.
Loading history...
351
                ? new Generator\BigIntegerIdentityGenerator()
352 2
                : new Generator\IdentityGenerator();
353
354 2
            $valueGenerator = new Mapping\ValueGeneratorMetadata(Mapping\GeneratorType::IDENTITY, $generator);
355
356 2
            $ids[0]->setValueGenerator($valueGenerator);
357
        }
358 2
    }
359
360
    /**
361
     * Parse the given Column as FieldMetadata
362
     *
363
     * @return Mapping\FieldMetadata
364
     */
365 2
    private function convertColumnAnnotationToFieldMetadata(string $tableName, Column $column, string $fieldName)
366
    {
367 2
        $options       = [];
368 2
        $fieldMetadata = new Mapping\FieldMetadata($fieldName);
369
370 2
        $fieldMetadata->setType($column->getType());
371 2
        $fieldMetadata->setTableName($tableName);
372 2
        $fieldMetadata->setColumnName($column->getName());
373
374
        // Type specific elements
375 2
        switch ($column->getType()->getName()) {
376
            case Type::TARRAY:
377
            case Type::BLOB:
378
            case Type::GUID:
379
            case Type::JSON_ARRAY:
380
            case Type::OBJECT:
381
            case Type::SIMPLE_ARRAY:
382
            case Type::STRING:
383
            case Type::TEXT:
384 1
                if ($column->getLength()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $column->getLength() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
385
                    $fieldMetadata->setLength($column->getLength());
386
                }
387
388 1
                $options['fixed'] = $column->getFixed();
389 1
                break;
390
391
            case Type::DECIMAL:
392
            case Type::FLOAT:
393
                $fieldMetadata->setScale($column->getScale());
394
                $fieldMetadata->setPrecision($column->getPrecision());
395
                break;
396
397
            case Type::INTEGER:
398
            case Type::BIGINT:
399
            case Type::SMALLINT:
400 2
                $options['unsigned'] = $column->getUnsigned();
401 2
                break;
402
        }
403
404
        // Comment
405 2
        $comment = $column->getComment();
406 2
        if ($comment !== null) {
407
            $options['comment'] = $comment;
408
        }
409
410
        // Default
411 2
        $default = $column->getDefault();
412 2
        if ($default !== null) {
413
            $options['default'] = $default;
414
        }
415
416 2
        $fieldMetadata->setOptions($options);
417
418 2
        return $fieldMetadata;
419
    }
420
421
    /**
422
     * Build to one (one to one, many to one) association mapping from class metadata.
423
     */
424 2
    private function buildToOneAssociationMappings(Mapping\ClassMetadata $metadata)
425
    {
426 2
        $tableName   = $metadata->getTableName();
427 2
        $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]);
428 2
        $foreignKeys = $this->getTableForeignKeys($this->tables[$tableName]);
429
430 2
        foreach ($foreignKeys as $foreignKey) {
431
            $foreignTableName   = $foreignKey->getForeignTableName();
432
            $fkColumns          = $foreignKey->getColumns();
433
            $fkForeignColumns   = $foreignKey->getForeignColumns();
434
            $localColumn        = current($fkColumns);
435
            $associationMapping = [
436
                'fieldName'    => $this->getFieldNameForColumn($tableName, $localColumn, true),
437
                'targetEntity' => $this->getClassNameForTable($foreignTableName),
438
            ];
439
440
            if ($metadata->getProperty($associationMapping['fieldName'])) {
441
                $associationMapping['fieldName'] .= '2'; // "foo" => "foo2"
442
            }
443
444
            if ($primaryKeys && in_array($localColumn, $primaryKeys, true)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $primaryKeys of type Doctrine\DBAL\Schema\Identifier[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
445
                $associationMapping['id'] = true;
446
            }
447
448
            for ($i = 0, $l = count($fkColumns); $i < $l; $i++) {
449
                $joinColumn = new Mapping\JoinColumnMetadata();
450
451
                $joinColumn->setColumnName($fkColumns[$i]);
452
                $joinColumn->setReferencedColumnName($fkForeignColumns[$i]);
453
454
                $associationMapping['joinColumns'][] = $joinColumn;
455
            }
456
457
            // Here we need to check if $fkColumns are the same as $primaryKeys
458
            if (! array_diff($fkColumns, $primaryKeys)) {
459
                $metadata->addProperty($associationMapping);
0 ignored issues
show
Bug introduced by
$associationMapping of type array<string,string> is incompatible with the type Doctrine\ORM\Mapping\Property expected by parameter $property of Doctrine\ORM\Mapping\ClassMetadata::addProperty(). ( Ignorable by Annotation )

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

459
                $metadata->addProperty(/** @scrutinizer ignore-type */ $associationMapping);
Loading history...
460
            } else {
461
                $metadata->addProperty($associationMapping);
462
            }
463
        }
464 2
    }
465
466
    /**
467
     * Retrieve schema table definition foreign keys.
468
     *
469
     * @return ForeignKeyConstraint[]
470
     */
471 2
    private function getTableForeignKeys(Table $table)
472
    {
473 2
        return $this->sm->getDatabasePlatform()->supportsForeignKeyConstraints()
474
            ? $table->getForeignKeys()
475 2
            : [];
476
    }
477
478
    /**
479
     * Retrieve schema table definition primary keys.
480
     *
481
     * @return Identifier[]
482
     */
483 2
    private function getTablePrimaryKeys(Table $table)
484
    {
485
        try {
486 2
            return $table->getPrimaryKey()->getColumns();
487
        } catch (SchemaException $e) {
488
            // Do nothing
489
        }
490
491
        return [];
492
    }
493
494
    /**
495
     * Returns the mapped class name for a table if it exists. Otherwise return "classified" version.
496
     *
497
     * @param string $tableName
498
     *
499
     * @return string
500
     */
501 2
    private function getClassNameForTable($tableName)
502
    {
503 2
        return $this->namespace . (
504 2
            $this->classNamesForTables[$tableName]
0 ignored issues
show
Bug introduced by
Are you sure $this->classNamesForTabl...strtolower($tableName)) of type Doctrine\DBAL\Schema\Table|string can be used in concatenation? ( Ignorable by Annotation )

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

504
            /** @scrutinizer ignore-type */ $this->classNamesForTables[$tableName]
Loading history...
505 2
                ?? Inflector::classify(strtolower($tableName))
506
        );
507
    }
508
509
    /**
510
     * Return the mapped field name for a column, if it exists. Otherwise return camelized version.
511
     *
512
     * @param string $tableName
513
     * @param string $columnName
514
     * @param bool   $fk         Whether the column is a foreignkey or not.
515
     *
516
     * @return string
517
     */
518 2
    private function getFieldNameForColumn($tableName, $columnName, $fk = false)
519
    {
520 2
        if (isset($this->fieldNamesForColumns[$tableName], $this->fieldNamesForColumns[$tableName][$columnName])) {
521
            return $this->fieldNamesForColumns[$tableName][$columnName];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->fieldNames...tableName][$columnName] returns the type Doctrine\DBAL\Schema\Table which is incompatible with the documented return type string.
Loading history...
522
        }
523
524 2
        $columnName = strtolower($columnName);
525
526
        // Replace _id if it is a foreignkey column
527 2
        if ($fk) {
528
            $columnName = str_replace('_id', '', $columnName);
529
        }
530
531 2
        return Inflector::camelize($columnName);
532
    }
533
}
534