Failed Conditions
Push — develop ( 856053...a7d1bd )
by Guilherme
61:28
created

DatabaseDriver::getTableForeignKeys()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
218
                        $joinColumn = new Mapping\JoinColumnMetadata();
219
220
                        $joinColumn->setColumnName($cols[$i]);
221
                        $joinColumn->setReferencedColumnName($fkCols[$i]);
222
223
                        $joinTable->addJoinColumn($joinColumn);
224
                    }
225
226
                    $fkCols = $otherFk->getForeignColumns();
227
                    $cols = $otherFk->getColumns();
228
229 View Code Duplication
                    for ($i = 0, $l = count($cols); $i < $l; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
230
                        $joinColumn = new Mapping\JoinColumnMetadata();
231
232
                        $joinColumn->setColumnName($cols[$i]);
233
                        $joinColumn->setReferencedColumnName($fkCols[$i]);
234
235
                        $joinTable->addInverseJoinColumn($joinColumn);
236
                    }
237
                } else {
238
                    $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true);
239
                }
240
241
                $metadata->addProperty($associationMapping);
0 ignored issues
show
Documentation introduced by
$associationMapping is of type array<string,string|obje...ing\JoinTableMetadata>>, but the function expects a object<Doctrine\ORM\Mapping\Property>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
242
243
                break;
244
            }
245
        }
246
    }
247
248
    /**
249
     * @return void
250
     *
251
     * @throws Mapping\MappingException
252 1
     */
253
    private function reverseEngineerMappingFromDatabase()
254
    {
255 2
        if ($this->tables !== null) {
256
            return;
257
        }
258
259
        $tables = [];
260
261
        foreach ($this->sm->listTableNames() as $tableName) {
262 2
            $tables[$tableName] = $this->sm->listTableDetails($tableName);
263
        }
264 2
265 2
        $this->tables = $this->manyToManyTables = $this->classToTableNames = [];
266
267
        foreach ($tables as $tableName => $table) {
268
            $foreignKeys = ($this->sm->getDatabasePlatform()->supportsForeignKeyConstraints())
269
                ? $table->getForeignKeys()
270
                : [];
271
272
            $allForeignKeyColumns = [];
273
274
            foreach ($foreignKeys as $foreignKey) {
275
                $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns());
276
            }
277
278
            if ( ! $table->hasPrimaryKey()) {
279
                throw new Mapping\MappingException(
280
                    "Table " . $table->getName() . " has no primary key. Doctrine does not ".
281
                    "support reverse engineering from tables that don't have a primary key."
282
                );
283
            }
284
285
            $pkColumns = $table->getPrimaryKey()->getColumns();
286
287
            sort($pkColumns);
288
            sort($allForeignKeyColumns);
289
290
            if ($pkColumns == $allForeignKeyColumns && count($foreignKeys) == 2) {
291
                $this->manyToManyTables[$tableName] = $table;
292
            } else {
293
                // lower-casing is necessary because of Oracle Uppercase Tablenames,
294
                // assumption is lower-case + underscore separated.
295
                $className = $this->getClassNameForTable($tableName);
296
297
                $this->tables[$tableName] = $table;
298
                $this->classToTableNames[$className] = $tableName;
299
            }
300
        }
301
    }
302
303
    /**
304
     * Build table from a class metadata.
305
     *
306
     * @param Mapping\ClassMetadata $metadata
307
     */
308
    private function buildTable(Mapping\ClassMetadata $metadata)
309
    {
310
        $tableName      = $this->classToTableNames[$metadata->getClassName()];
311
        $indexes        = $this->tables[$tableName]->getIndexes();
312
        $tableMetadata  = new Mapping\TableMetadata();
313
314
        $tableMetadata->setName($this->classToTableNames[$metadata->getClassName()]);
315
316
        foreach ($indexes as $index) {
317 2
            /** @var Index $index */
318
            if ($index->isPrimary()) {
319 2
                continue;
320 2
            }
321
322 2
            $tableMetadata->addIndex([
323 2
                'name'    => $index->getName(),
324 2
                'columns' => $index->getColumns(),
325
                'unique'  => $index->isUnique(),
326
                'options' => $index->getOptions(),
327 1
                'flags'   => $index->getFlags(),
328 1
            ]);
329 1
        }
330
331 1
        $metadata->setTable($tableMetadata);
332
    }
333 1
334
    /**
335 2
     * Build field mapping from class metadata.
336
     *
337
     * @param Mapping\ClassMetadata $metadata
338
     */
339
    private function buildFieldMappings(Mapping\ClassMetadata $metadata)
340
    {
341
        $tableName      = $metadata->getTableName();
342 2
        $columns        = $this->tables[$tableName]->getColumns();
343
        $primaryKeys    = $this->getTablePrimaryKeys($this->tables[$tableName]);
344 2
        $foreignKeys    = $this->getTableForeignKeys($this->tables[$tableName]);
345 2
        $allForeignKeys = [];
346 2
347 2
        foreach ($foreignKeys as $foreignKey) {
348 2
            $allForeignKeys = array_merge($allForeignKeys, $foreignKey->getLocalColumns());
349
        }
350 2
351
        $ids = [];
352
353
        foreach ($columns as $column) {
354 2
            if (in_array($column->getName(), $allForeignKeys)) {
355
                continue;
356 2
            }
357 2
358
            $fieldName     = $this->getFieldNameForColumn($tableName, $column->getName(), false);
359
            $fieldMetadata = $this->convertColumnAnnotationToFieldMetadata($tableName, $column, $fieldName);
360
361 2
            if ($primaryKeys && in_array($column->getName(), $primaryKeys)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $primaryKeys of type array 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...
362 2
                $fieldMetadata->setPrimaryKey(true);
363
364 2
                $ids[] = $fieldMetadata;
365 2
            }
366
367 2
            $metadata->addProperty($fieldMetadata);
368
        }
369
370 2
        // We need to check for the columns here, because we might have associations as id as well.
371
        if ($ids && count($primaryKeys) === 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids of type array 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...
372
            $ids[0]->setValueGenerator(new Mapping\ValueGeneratorMetadata(Mapping\GeneratorType::AUTO));
373
        }
374 2
    }
375 2
376
    /**
377 2
     * Parse the given Column as FieldMetadata
378
     *
379
     * @param string $tableName
380
     * @param Column $column
381
     * @param string $fieldName
382
     *
383
     * @return Mapping\FieldMetadata
384
     */
385
    private function convertColumnAnnotationToFieldMetadata(string $tableName, Column $column, string $fieldName)
386
    {
387 2
        $options       = [];
388
        $fieldMetadata = new Mapping\FieldMetadata($fieldName);
389
390 2
        $fieldMetadata->setType($column->getType());
391 2
        $fieldMetadata->setTableName($tableName);
392
        $fieldMetadata->setColumnName($column->getName());
393
394
        // Type specific elements
395 2
        switch ($column->getType()->getName()) {
396 2
            case Type::TARRAY:
397 2
            case Type::BLOB:
398 2
            case Type::GUID:
399 2
            case Type::JSON_ARRAY:
400 2
            case Type::OBJECT:
401 2
            case Type::SIMPLE_ARRAY:
402 2
            case Type::STRING:
403 2
            case Type::TEXT:
404 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 zero. 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...
405 1
                    $fieldMetadata->setLength($column->getLength());
406 1
                }
407
408 2
                $options['fixed'] = $column->getFixed();
409 2
                break;
410
411
            case Type::DECIMAL:
412
            case Type::FLOAT:
413
                $fieldMetadata->setScale($column->getScale());
414 2
                $fieldMetadata->setPrecision($column->getPrecision());
415
                break;
416
417 2
            case Type::INTEGER:
418 2
            case Type::BIGINT:
419
            case Type::SMALLINT:
420
                $options['unsigned'] = $column->getUnsigned();
421
                break;
422 2
        }
423
424
        // Comment
425
        if (($comment = $column->getComment()) !== null) {
426
            $options['comment'] = $comment;
427 2
        }
428
429
        // Default
430
        if (($default = $column->getDefault()) !== null) {
431 2
            $options['default'] = $default;
432
        }
433
434
        $fieldMetadata->setOptions($options);
435
436
        return $fieldMetadata;
437
    }
438
439 2
    /**
440
     * Build to one (one to one, many to one) association mapping from class metadata.
441 2
     *
442 2
     * @param Mapping\ClassMetadata $metadata
443 2
     */
444
    private function buildToOneAssociationMappings(Mapping\ClassMetadata $metadata)
445 2
    {
446
        $tableName   = $metadata->getTableName();
447
        $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]);
448
        $foreignKeys = $this->getTableForeignKeys($this->tables[$tableName]);
449
450
        foreach ($foreignKeys as $foreignKey) {
451
            $foreignTableName   = $foreignKey->getForeignTableName();
452
            $fkColumns          = $foreignKey->getColumns();
453
            $fkForeignColumns   = $foreignKey->getForeignColumns();
454
            $localColumn        = current($fkColumns);
455
            $associationMapping = [
456
                'fieldName'    => $this->getFieldNameForColumn($tableName, $localColumn, true),
457
                'targetEntity' => $this->getClassNameForTable($foreignTableName),
458
            ];
459
460
            if ($metadata->getProperty($associationMapping['fieldName'])) {
461
                $associationMapping['fieldName'] .= '2'; // "foo" => "foo2"
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
462
            }
463
464
            if ($primaryKeys && in_array($localColumn, $primaryKeys)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $primaryKeys of type array 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...
465
                $associationMapping['id'] = true;
466
            }
467
468 View Code Duplication
            for ($i = 0, $l = count($fkColumns); $i < $l; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
469
                $joinColumn = new Mapping\JoinColumnMetadata();
470
471
                $joinColumn->setColumnName($fkColumns[$i]);
472
                $joinColumn->setReferencedColumnName($fkForeignColumns[$i]);
473
474
                $associationMapping['joinColumns'][] = $joinColumn;
475
            }
476
477 2
            // Here we need to check if $fkColumns are the same as $primaryKeys
478
            if ( ! array_diff($fkColumns, $primaryKeys)) {
479
                $metadata->addProperty($associationMapping);
0 ignored issues
show
Documentation introduced by
$associationMapping is of type array<string,string|boolean>, but the function expects a object<Doctrine\ORM\Mapping\Property>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
480
            } else {
481
                $metadata->addProperty($associationMapping);
0 ignored issues
show
Documentation introduced by
$associationMapping is of type array<string,string|boolean>, but the function expects a object<Doctrine\ORM\Mapping\Property>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
482
            }
483
        }
484
    }
485
486 2
    /**
487
     * Retrieve schema table definition foreign keys.
488 2
     *
489
     * @param \Doctrine\DBAL\Schema\Table $table
490 2
     *
491
     * @return array
492
     */
493
    private function getTableForeignKeys(Table $table)
494
    {
495
        return ($this->sm->getDatabasePlatform()->supportsForeignKeyConstraints())
496
            ? $table->getForeignKeys()
497
            : [];
498
    }
499
500 2
    /**
501
     * Retrieve schema table definition primary keys.
502
     *
503 2
     * @param \Doctrine\DBAL\Schema\Table $table
504
     *
505
     * @return array
506
     */
507
    private function getTablePrimaryKeys(Table $table)
508
    {
509
        try {
510
            return $table->getPrimaryKey()->getColumns();
511
        } catch (SchemaException $e) {
512
            // Do nothing
513
        }
514
515
        return [];
516
    }
517
518 2
    /**
519
     * Returns the mapped class name for a table if it exists. Otherwise return "classified" version.
520 2
     *
521
     * @param string $tableName
522
     *
523
     * @return string
524 2
     */
525
    private function getClassNameForTable($tableName)
526
    {
527
        if (isset($this->classNamesForTables[$tableName])) {
528
            return $this->namespace . $this->classNamesForTables[$tableName];
529
        }
530
531
        return $this->namespace . Inflector::classify(strtolower($tableName));
532
    }
533
534
    /**
535
     * Return the mapped field name for a column, if it exists. Otherwise return camelized version.
536 2
     *
537
     * @param string  $tableName
538 2
     * @param string  $columnName
539
     * @param boolean $fk Whether the column is a foreignkey or not.
540
     *
541
     * @return string
542 2
     */
543
    private function getFieldNameForColumn($tableName, $columnName, $fk = false)
544
    {
545 2
        if (isset($this->fieldNamesForColumns[$tableName]) && isset($this->fieldNamesForColumns[$tableName][$columnName])) {
546
            return $this->fieldNamesForColumns[$tableName][$columnName];
547
        }
548
549 2
        $columnName = strtolower($columnName);
550
551
        // Replace _id if it is a foreignkey column
552
        if ($fk) {
553
            $columnName = str_replace('_id', '', $columnName);
554
        }
555
556
        return Inflector::camelize($columnName);
557
    }
558
}
559