Failed Conditions
Push — master ( e747f7...5b15a6 )
by Guilherme
19:57
created

DatabaseDriver::loadMetadataForClass()   B

Complexity

Conditions 11
Paths 13

Size

Total Lines 90
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 37.6887

Importance

Changes 0
Metric Value
cc 11
eloc 48
nc 13
nop 3
dl 0
loc 90
ccs 19
cts 48
cp 0.3958
crap 37.6887
rs 7.3166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

336
            $fieldMetadata = $this->convertColumnAnnotationToFieldMetadata(/** @scrutinizer ignore-type */ $tableName, $column, $fieldName);
Loading history...
337
338 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...
339 2
                $fieldMetadata->setPrimaryKey(true);
340
341 2
                $ids[] = $fieldMetadata;
342
            }
343
344 2
            $metadata->addProperty($fieldMetadata);
345
        }
346
347
        // We need to check for the columns here, because we might have associations as id as well.
348 2
        if ($ids && count($primaryKeys) === 1) {
349 2
            $ids[0]->setValueGenerator(new Mapping\ValueGeneratorMetadata(Mapping\GeneratorType::AUTO));
350
        }
351 2
    }
352
353
    /**
354
     * Parse the given Column as FieldMetadata
355
     *
356
     * @return Mapping\FieldMetadata
357
     */
358 2
    private function convertColumnAnnotationToFieldMetadata(string $tableName, Column $column, string $fieldName)
359
    {
360 2
        $options       = [];
361 2
        $fieldMetadata = new Mapping\FieldMetadata($fieldName);
362
363 2
        $fieldMetadata->setType($column->getType());
364 2
        $fieldMetadata->setTableName($tableName);
365 2
        $fieldMetadata->setColumnName($column->getName());
366
367
        // Type specific elements
368 2
        switch ($column->getType()->getName()) {
369
            case Type::TARRAY:
370
            case Type::BLOB:
371
            case Type::GUID:
372
            case Type::JSON_ARRAY:
373
            case Type::OBJECT:
374
            case Type::SIMPLE_ARRAY:
375
            case Type::STRING:
376
            case Type::TEXT:
377 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...
378
                    $fieldMetadata->setLength($column->getLength());
379
                }
380
381 1
                $options['fixed'] = $column->getFixed();
382 1
                break;
383
384
            case Type::DECIMAL:
385
            case Type::FLOAT:
386
                $fieldMetadata->setScale($column->getScale());
387
                $fieldMetadata->setPrecision($column->getPrecision());
388
                break;
389
390
            case Type::INTEGER:
391
            case Type::BIGINT:
392
            case Type::SMALLINT:
393 2
                $options['unsigned'] = $column->getUnsigned();
394 2
                break;
395
        }
396
397
        // Comment
398 2
        $comment = $column->getComment();
399 2
        if ($comment !== null) {
400
            $options['comment'] = $comment;
401
        }
402
403
        // Default
404 2
        $default = $column->getDefault();
405 2
        if ($default !== null) {
406
            $options['default'] = $default;
407
        }
408
409 2
        $fieldMetadata->setOptions($options);
410
411 2
        return $fieldMetadata;
412
    }
413
414
    /**
415
     * Build to one (one to one, many to one) association mapping from class metadata.
416
     */
417 2
    private function buildToOneAssociationMappings(Mapping\ClassMetadata $metadata)
418
    {
419 2
        $tableName   = $metadata->getTableName();
420 2
        $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]);
421 2
        $foreignKeys = $this->getTableForeignKeys($this->tables[$tableName]);
422
423 2
        foreach ($foreignKeys as $foreignKey) {
424
            $foreignTableName   = $foreignKey->getForeignTableName();
425
            $fkColumns          = $foreignKey->getColumns();
426
            $fkForeignColumns   = $foreignKey->getForeignColumns();
427
            $localColumn        = current($fkColumns);
428
            $associationMapping = [
429
                'fieldName'    => $this->getFieldNameForColumn($tableName, $localColumn, true),
430
                'targetEntity' => $this->getClassNameForTable($foreignTableName),
431
            ];
432
433
            if ($metadata->getProperty($associationMapping['fieldName'])) {
434
                $associationMapping['fieldName'] .= '2'; // "foo" => "foo2"
435
            }
436
437
            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...
438
                $associationMapping['id'] = true;
439
            }
440
441
            for ($i = 0, $l = count($fkColumns); $i < $l; $i++) {
442
                $joinColumn = new Mapping\JoinColumnMetadata();
443
444
                $joinColumn->setColumnName($fkColumns[$i]);
445
                $joinColumn->setReferencedColumnName($fkForeignColumns[$i]);
446
447
                $associationMapping['joinColumns'][] = $joinColumn;
448
            }
449
450
            // Here we need to check if $fkColumns are the same as $primaryKeys
451
            if (! array_diff($fkColumns, $primaryKeys)) {
452
                $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

452
                $metadata->addProperty(/** @scrutinizer ignore-type */ $associationMapping);
Loading history...
453
            } else {
454
                $metadata->addProperty($associationMapping);
455
            }
456
        }
457 2
    }
458
459
    /**
460
     * Retrieve schema table definition foreign keys.
461
     *
462
     * @return ForeignKeyConstraint[]
463
     */
464 2
    private function getTableForeignKeys(Table $table)
465
    {
466 2
        return $this->sm->getDatabasePlatform()->supportsForeignKeyConstraints()
467
            ? $table->getForeignKeys()
468 2
            : [];
469
    }
470
471
    /**
472
     * Retrieve schema table definition primary keys.
473
     *
474
     * @return Identifier[]
475
     */
476 2
    private function getTablePrimaryKeys(Table $table)
477
    {
478
        try {
479 2
            return $table->getPrimaryKey()->getColumns();
480
        } catch (SchemaException $e) {
481
            // Do nothing
482
        }
483
484
        return [];
485
    }
486
487
    /**
488
     * Returns the mapped class name for a table if it exists. Otherwise return "classified" version.
489
     *
490
     * @param string $tableName
491
     *
492
     * @return string
493
     */
494 2
    private function getClassNameForTable($tableName)
495
    {
496 2
        return $this->namespace . (
497 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

497
            /** @scrutinizer ignore-type */ $this->classNamesForTables[$tableName]
Loading history...
498 2
                ?? Inflector::classify(strtolower($tableName))
499
        );
500
    }
501
502
    /**
503
     * Return the mapped field name for a column, if it exists. Otherwise return camelized version.
504
     *
505
     * @param string $tableName
506
     * @param string $columnName
507
     * @param bool   $fk         Whether the column is a foreignkey or not.
508
     *
509
     * @return string
510
     */
511 2
    private function getFieldNameForColumn($tableName, $columnName, $fk = false)
512
    {
513 2
        if (isset($this->fieldNamesForColumns[$tableName], $this->fieldNamesForColumns[$tableName][$columnName])) {
514
            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...
515
        }
516
517 2
        $columnName = strtolower($columnName);
518
519
        // Replace _id if it is a foreignkey column
520 2
        if ($fk) {
521
            $columnName = str_replace('_id', '', $columnName);
522
        }
523
524 2
        return Inflector::camelize($columnName);
525
    }
526
}
527