Failed Conditions
Pull Request — 2.6 (#7180)
by Ben
11:16
created

DatabaseDriver::setFieldNameForColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 3
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
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\ORM\Mapping\Driver;
21
22
use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver;
23
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
24
use Doctrine\Common\Util\Inflector;
25
use Doctrine\DBAL\Schema\AbstractSchemaManager;
26
use Doctrine\DBAL\Schema\SchemaException;
27
use Doctrine\DBAL\Schema\Table;
28
use Doctrine\DBAL\Schema\Column;
29
use Doctrine\DBAL\Types\Type;
30
use Doctrine\ORM\Mapping\ClassMetadataInfo;
31
use Doctrine\ORM\Mapping\MappingException;
32
33
/**
34
 * The DatabaseDriver reverse engineers the mapping metadata from a database.
35
 *
36
 * @link    www.doctrine-project.org
37
 * @since   2.0
38
 * @author  Guilherme Blanco <[email protected]>
39
 * @author  Jonathan Wage <[email protected]>
40
 * @author  Benjamin Eberlei <[email protected]>
41
 */
42
class DatabaseDriver implements MappingDriver
43
{
44
    /**
45
     * @var AbstractSchemaManager
46
     */
47
    private $_sm;
48
49
    /**
50
     * @var array|null
51
     */
52
    private $tables = null;
53
54
    /**
55
     * @var array
56
     */
57
    private $classToTableNames = [];
58
59
    /**
60
     * @var array
61
     */
62
    private $manyToManyTables = [];
63
64
    /**
65
     * @var array
66
     */
67
    private $classNamesForTables = [];
68
69
    /**
70
     * @var array
71
     */
72
    private $fieldNamesForColumns = [];
73
74
    /**
75
     * The namespace for the generated entities.
76
     *
77
     * @var string|null
78
     */
79
    private $namespace;
80
81
    /**
82
     * @param AbstractSchemaManager $schemaManager
83
     */
84 2
    public function __construct(AbstractSchemaManager $schemaManager)
85
    {
86 2
        $this->_sm = $schemaManager;
87 2
    }
88
89
    /**
90
     * Set the namespace for the generated entities.
91
     *
92
     * @param string $namespace
93
     *
94
     * @return void
95
     */
96
    public function setNamespace($namespace)
97
    {
98
        $this->namespace = $namespace;
99
    }
100
101
    /**
102
     * {@inheritDoc}
103
     */
104
    public function isTransient($className)
105
    {
106
        return true;
107
    }
108
109
    /**
110
     * {@inheritDoc}
111
     */
112 2
    public function getAllClassNames()
113
    {
114 2
        $this->reverseEngineerMappingFromDatabase();
115
116 2
        return array_keys($this->classToTableNames);
117
    }
118
119
    /**
120
     * Sets class name for a table.
121
     *
122
     * @param string $tableName
123
     * @param string $className
124
     *
125
     * @return void
126
     */
127
    public function setClassNameForTable($tableName, $className)
128
    {
129
        $this->classNamesForTables[$tableName] = $className;
130
    }
131
132
    /**
133
     * Sets field name for a column on a specific table.
134
     *
135
     * @param string $tableName
136
     * @param string $columnName
137
     * @param string $fieldName
138
     *
139
     * @return void
140
     */
141
    public function setFieldNameForColumn($tableName, $columnName, $fieldName)
142
    {
143
        $this->fieldNamesForColumns[$tableName][$columnName] = $fieldName;
144
    }
145
146
    /**
147
     * Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager.
148
     *
149
     * @param array $entityTables
150
     * @param array $manyToManyTables
151
     *
152
     * @return void
153
     */
154 2
    public function setTables($entityTables, $manyToManyTables)
155
    {
156 2
        $this->tables = $this->manyToManyTables = $this->classToTableNames = [];
157
158 2
        foreach ($entityTables as $table) {
159 2
            $className = $this->getClassNameForTable($table->getName());
160
161 2
            $this->classToTableNames[$className] = $table->getName();
162 2
            $this->tables[$table->getName()] = $table;
163
        }
164
165 2
        foreach ($manyToManyTables as $table) {
166 1
            $this->manyToManyTables[$table->getName()] = $table;
167
        }
168 2
    }
169
170
    /**
171
     * {@inheritDoc}
172
     */
173 2
    public function loadMetadataForClass($className, ClassMetadata $metadata)
174
    {
175 2
        $this->reverseEngineerMappingFromDatabase();
176
177 2
        if ( ! isset($this->classToTableNames[$className])) {
178
            throw new \InvalidArgumentException("Unknown class " . $className);
179
        }
180
181 2
        $tableName = $this->classToTableNames[$className];
182
183 2
        $metadata->name = $className;
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
184 2
        $metadata->table['name'] = $tableName;
0 ignored issues
show
Bug introduced by
Accessing table on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
185
186 2
        $this->buildIndexes($metadata);
187 2
        $this->buildFieldMappings($metadata);
188 2
        $this->buildToOneAssociationMappings($metadata);
189
190 2
        foreach ($this->manyToManyTables as $manyTable) {
191 1
            foreach ($manyTable->getForeignKeys() as $foreignKey) {
192
                // foreign key maps to the table of the current entity, many to many association probably exists
193 1
                if ( ! (strtolower($tableName) === strtolower($foreignKey->getForeignTableName()))) {
194 1
                    continue;
195
                }
196
197 1
                $myFk = $foreignKey;
198 1
                $otherFk = null;
199
200 1
                foreach ($manyTable->getForeignKeys() as $foreignKey) {
0 ignored issues
show
Comprehensibility Bug introduced by
$foreignKey is overwriting a variable from outer foreach loop.
Loading history...
201 1
                    if ($foreignKey != $myFk) {
202
                        $otherFk = $foreignKey;
203 1
                        break;
204
                    }
205
                }
206
207 1
                if ( ! $otherFk) {
208
                    // the definition of this many to many table does not contain
209
                    // enough foreign key information to continue reverse engineering.
210 1
                    continue;
211
                }
212
213
                $localColumn = current($myFk->getColumns());
214
215
                $associationMapping = [];
216
                $associationMapping['fieldName'] = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getColumns()), true);
217
                $associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName());
218
219
                if (current($manyTable->getColumns())->getName() == $localColumn) {
220
                    $associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true);
221
                    $associationMapping['joinTable'] = [
222
                        'name' => strtolower($manyTable->getName()),
223
                        'joinColumns' => [],
224
                        'inverseJoinColumns' => [],
225
                    ];
226
227
                    $fkCols = $myFk->getForeignColumns();
228
                    $cols = $myFk->getColumns();
229
230
                    for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) {
231
                        $associationMapping['joinTable']['joinColumns'][] = [
232
                            'name' => $cols[$i],
233
                            'referencedColumnName' => $fkCols[$i],
234
                        ];
235
                    }
236
237
                    $fkCols = $otherFk->getForeignColumns();
238
                    $cols = $otherFk->getColumns();
239
240
                    for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) {
241
                        $associationMapping['joinTable']['inverseJoinColumns'][] = [
242
                            'name' => $cols[$i],
243
                            'referencedColumnName' => $fkCols[$i],
244
                        ];
245
                    }
246
                } else {
247
                    $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true);
248
                }
249
250
                $metadata->mapManyToMany($associationMapping);
0 ignored issues
show
Bug introduced by
The method mapManyToMany() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Common\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

250
                $metadata->/** @scrutinizer ignore-call */ 
251
                           mapManyToMany($associationMapping);
Loading history...
251
252 1
                break;
253
            }
254
        }
255 2
    }
256
257
    /**
258
     * @return void
259
     *
260
     * @throws \Doctrine\ORM\Mapping\MappingException
261
     */
262 2
    private function reverseEngineerMappingFromDatabase()
263
    {
264 2
        if ($this->tables !== null) {
265 2
            return;
266
        }
267
268
        $tables = [];
269
270
        foreach ($this->_sm->listTableNames() as $tableName) {
271
            $tables[$tableName] = $this->_sm->listTableDetails($tableName);
272
        }
273
274
        $this->tables = $this->manyToManyTables = $this->classToTableNames = [];
275
276
        foreach ($tables as $tableName => $table) {
277
            $foreignKeys = ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints())
278
                ? $table->getForeignKeys()
279
                : [];
280
281
            $allForeignKeyColumns = [];
282
283
            foreach ($foreignKeys as $foreignKey) {
284
                $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns());
285
            }
286
287
            if ( ! $table->hasPrimaryKey()) {
288
                throw new MappingException(
289
                    "Table " . $table->getName() . " has no primary key. Doctrine does not ".
290
                    "support reverse engineering from tables that don't have a primary key."
291
                );
292
            }
293
294
            $pkColumns = $table->getPrimaryKey()->getColumns();
295
296
            sort($pkColumns);
297
            sort($allForeignKeyColumns);
298
299
            if ($pkColumns == $allForeignKeyColumns && count($foreignKeys) == 2) {
300
                $this->manyToManyTables[$tableName] = $table;
301
            } else {
302
                // lower-casing is necessary because of Oracle Uppercase Tablenames,
303
                // assumption is lower-case + underscore separated.
304
                $className = $this->getClassNameForTable($tableName);
305
306
                $this->tables[$tableName] = $table;
307
                $this->classToTableNames[$className] = $tableName;
308
            }
309
        }
310
    }
311
312
    /**
313
     * Build indexes from a class metadata.
314
     *
315
     * @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata
316
     */
317 2
    private function buildIndexes(ClassMetadataInfo $metadata)
318
    {
319 2
        $tableName = $metadata->table['name'];
320 2
        $indexes   = $this->tables[$tableName]->getIndexes();
321
322 2
        foreach ($indexes as $index) {
323 2
            if ($index->isPrimary()) {
324 2
                continue;
325
            }
326
327 1
            $indexName      = $index->getName();
328 1
            $indexColumns   = $index->getColumns();
329 1
            $constraintType = $index->isUnique()
330
                ? 'uniqueConstraints'
331 1
                : 'indexes';
332
333 1
            $metadata->table[$constraintType][$indexName]['columns'] = $indexColumns;
334
        }
335 2
    }
336
337
    /**
338
     * Build field mapping from class metadata.
339
     *
340
     * @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata
341
     */
342 2
    private function buildFieldMappings(ClassMetadataInfo $metadata)
343
    {
344 2
        $tableName      = $metadata->table['name'];
345 2
        $columns        = $this->tables[$tableName]->getColumns();
346 2
        $primaryKeys    = $this->getTablePrimaryKeys($this->tables[$tableName]);
347 2
        $foreignKeys    = $this->getTableForeignKeys($this->tables[$tableName]);
348 2
        $allForeignKeys = [];
349
350 2
        foreach ($foreignKeys as $foreignKey) {
351
            $allForeignKeys = array_merge($allForeignKeys, $foreignKey->getLocalColumns());
352
        }
353
354 2
        $ids           = [];
355 2
        $fieldMappings = [];
356
357 2
        foreach ($columns as $column) {
358 2
            if (in_array($column->getName(), $allForeignKeys)) {
359
                continue;
360
            }
361
362 2
            $fieldMapping = $this->buildFieldMapping($tableName, $column);
363
364 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...
365 2
                $fieldMapping['id'] = true;
366 2
                $ids[] = $fieldMapping;
367
            }
368
369 2
            $fieldMappings[] = $fieldMapping;
370
        }
371
372
        // We need to check for the columns here, because we might have associations as id as well.
373 2
        if ($ids && count($primaryKeys) == 1) {
374 2
            $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO);
375
        }
376
377 2
        foreach ($fieldMappings as $fieldMapping) {
378 2
            $metadata->mapField($fieldMapping);
379
        }
380 2
    }
381
382
    /**
383
     * Build field mapping from a schema column definition
384
     *
385
     * @param string                       $tableName
386
     * @param \Doctrine\DBAL\Schema\Column $column
387
     *
388
     * @return array
389
     */
390 2
    private function buildFieldMapping($tableName, Column $column)
391
    {
392
        $fieldMapping = [
393 2
            'fieldName'  => $this->getFieldNameForColumn($tableName, $column->getName(), false),
394 2
            'columnName' => $column->getName(),
395 2
            'type'       => $column->getType()->getName(),
396 2
            'nullable'   => ( ! $column->getNotnull()),
397
        ];
398
399
        // Type specific elements
400 2
        switch ($fieldMapping['type']) {
401 2
            case Type::TARRAY:
402 2
            case Type::BLOB:
403 2
            case Type::GUID:
404 2
            case Type::JSON_ARRAY:
405 2
            case Type::OBJECT:
406 2
            case Type::SIMPLE_ARRAY:
407 2
            case Type::STRING:
408 2
            case Type::TEXT:
409 1
                $fieldMapping['length'] = $column->getLength();
410 1
                $fieldMapping['options']['fixed']  = $column->getFixed();
411 1
                break;
412
413 2
            case Type::DECIMAL:
414 2
            case Type::FLOAT:
415
                $fieldMapping['precision'] = $column->getPrecision();
416
                $fieldMapping['scale']     = $column->getScale();
417
                break;
418
419 2
            case Type::INTEGER:
420
            case Type::BIGINT:
421
            case Type::SMALLINT:
422 2
                $fieldMapping['options']['unsigned'] = $column->getUnsigned();
423 2
                break;
424
        }
425
426
        // Comment
427 2
        if (($comment = $column->getComment()) !== null) {
428
            $fieldMapping['options']['comment'] = $comment;
429
        }
430
431
        // Default
432 2
        if (($default = $column->getDefault()) !== null) {
433
            $fieldMapping['options']['default'] = $default;
434
        }
435
436 2
        return $fieldMapping;
437
    }
438
439
    /**
440
     * Build to one (one to one, many to one) association mapping from class metadata.
441
     *
442
     * @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata
443
     */
444 2
    private function buildToOneAssociationMappings(ClassMetadataInfo $metadata)
445
    {
446 2
        $tableName   = $metadata->table['name'];
447 2
        $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]);
448 2
        $foreignKeys = $this->getTableForeignKeys($this->tables[$tableName]);
449
450 2
        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 (isset($metadata->fieldMappings[$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
            for ($i = 0, $fkColumnsCount = count($fkColumns); $i < $fkColumnsCount; $i++) {
469
                $associationMapping['joinColumns'][] = [
470
                    'name'                 => $fkColumns[$i],
471
                    'referencedColumnName' => $fkForeignColumns[$i],
472
                ];
473
            }
474
475
            // Here we need to check if $fkColumns are the same as $primaryKeys
476
            if ( ! array_diff($fkColumns, $primaryKeys)) {
477
                $metadata->mapOneToOne($associationMapping);
478
            } else {
479
                $metadata->mapManyToOne($associationMapping);
480
            }
481
        }
482 2
    }
483
484
    /**
485
     * Retrieve schema table definition foreign keys.
486
     *
487
     * @param \Doctrine\DBAL\Schema\Table $table
488
     *
489
     * @return array
490
     */
491 2
    private function getTableForeignKeys(Table $table)
492
    {
493 2
        return ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints())
494
            ? $table->getForeignKeys()
495 2
            : [];
496
    }
497
498
    /**
499
     * Retrieve schema table definition primary keys.
500
     *
501
     * @param \Doctrine\DBAL\Schema\Table $table
502
     *
503
     * @return array
504
     */
505 2
    private function getTablePrimaryKeys(Table $table)
506
    {
507
        try {
508 2
            return $table->getPrimaryKey()->getColumns();
509
        } catch (SchemaException $e) {
510
            // Do nothing
511
        }
512
513
        return [];
514
    }
515
516
    /**
517
     * Returns the mapped class name for a table if it exists. Otherwise return "classified" version.
518
     *
519
     * @param string $tableName
520
     *
521
     * @return string
522
     */
523 2
    private function getClassNameForTable($tableName)
524
    {
525 2
        if (isset($this->classNamesForTables[$tableName])) {
526
            return $this->namespace . $this->classNamesForTables[$tableName];
527
        }
528
529 2
        return $this->namespace . Inflector::classify(strtolower($tableName));
530
    }
531
532
    /**
533
     * Return the mapped field name for a column, if it exists. Otherwise return camelized version.
534
     *
535
     * @param string  $tableName
536
     * @param string  $columnName
537
     * @param boolean $fk Whether the column is a foreignkey or not.
538
     *
539
     * @return string
540
     */
541 2
    private function getFieldNameForColumn($tableName, $columnName, $fk = false)
542
    {
543 2
        if (isset($this->fieldNamesForColumns[$tableName]) && isset($this->fieldNamesForColumns[$tableName][$columnName])) {
544
            return $this->fieldNamesForColumns[$tableName][$columnName];
545
        }
546
547 2
        $columnName = strtolower($columnName);
548
549
        // Replace _id if it is a foreignkey column
550 2
        if ($fk) {
551
            $columnName = str_replace('_id', '', $columnName);
552
        }
553
554 2
        return Inflector::camelize($columnName);
555
    }
556
}
557