Completed
Push — master ( a0071b...e33605 )
by Michael
12s
created

lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php (1 issue)

Labels
Severity
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\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
18
/**
19
 * The DatabaseDriver reverse engineers the mapping metadata from a database.
20
 */
21
class DatabaseDriver implements MappingDriver
22
{
23
    /**
24
     * @var AbstractSchemaManager
25
     */
26
    private $sm;
27
28
    /**
29
     * @var Table[]|null
30
     */
31
    private $tables;
32
33
    /**
34
     * @var string[]
35
     */
36
    private $classToTableNames = [];
37
38
    /**
39
     * @var Table[]
40
     */
41
    private $manyToManyTables = [];
42
43
    /**
44
     * @var Table[]
45
     */
46
    private $classNamesForTables = [];
47
48
    /**
49
     * @var Table[]
50
     */
51
    private $fieldNamesForColumns = [];
52
53
    /**
54
     * The namespace for the generated entities.
55
     *
56
     * @var string|null
57
     */
58
    private $namespace;
59
60 2
    public function __construct(AbstractSchemaManager $schemaManager)
61
    {
62 2
        $this->sm = $schemaManager;
63 2
    }
64
65
    /**
66
     * Set the namespace for the generated entities.
67
     *
68
     * @param string $namespace
69
     */
70
    public function setNamespace($namespace)
71
    {
72
        $this->namespace = $namespace;
73
    }
74
75
    /**
76
     * {@inheritDoc}
77
     */
78
    public function isTransient($className)
79
    {
80
        return true;
81
    }
82
83
    /**
84
     * {@inheritDoc}
85
     */
86 2
    public function getAllClassNames()
87
    {
88 2
        $this->reverseEngineerMappingFromDatabase();
89
90 2
        return array_keys($this->classToTableNames);
91
    }
92
93
    /**
94
     * Sets class name for a table.
95
     *
96
     * @param string $tableName
97
     * @param string $className
98
     */
99
    public function setClassNameForTable($tableName, $className)
100
    {
101
        $this->classNamesForTables[$tableName] = $className;
102
    }
103
104
    /**
105
     * Sets field name for a column on a specific table.
106
     *
107
     * @param string $tableName
108
     * @param string $columnName
109
     * @param string $fieldName
110
     */
111
    public function setFieldNameForColumn($tableName, $columnName, $fieldName)
112
    {
113
        $this->fieldNamesForColumns[$tableName][$columnName] = $fieldName;
114
    }
115
116
    /**
117
     * Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager.
118
     *
119
     * @param Table[] $entityTables
120
     * @param Table[] $manyToManyTables
121
     */
122 2
    public function setTables($entityTables, $manyToManyTables)
123
    {
124 2
        $this->tables = $this->manyToManyTables = $this->classToTableNames = [];
125
126 2
        foreach ($entityTables as $table) {
127 2
            $className = $this->getClassNameForTable($table->getName());
128
129 2
            $this->classToTableNames[$className] = $table->getName();
130 2
            $this->tables[$table->getName()]     = $table;
131
        }
132
133 2
        foreach ($manyToManyTables as $table) {
134 1
            $this->manyToManyTables[$table->getName()] = $table;
135
        }
136 2
    }
137
138
    /**
139
     * {@inheritDoc}
140
     */
141 2
    public function loadMetadataForClass(
142
        string $className,
143
        Mapping\ClassMetadata $metadata,
144
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
145
    ) {
146 2
        $this->reverseEngineerMappingFromDatabase();
147
148 2
        if (! isset($this->classToTableNames[$className])) {
149
            throw new \InvalidArgumentException('Unknown class ' . $className);
150
        }
151
152
        // @todo guilhermeblanco This should somehow disappear... =)
153 2
        $metadata->setClassName($className);
154
155 2
        $this->buildTable($metadata);
156 2
        $this->buildFieldMappings($metadata);
157 2
        $this->buildToOneAssociationMappings($metadata);
158
159 2
        $loweredTableName = strtolower($metadata->getTableName());
160
161 2
        foreach ($this->manyToManyTables as $manyTable) {
162 1
            foreach ($manyTable->getForeignKeys() as $foreignKey) {
163
                // foreign key maps to the table of the current entity, many to many association probably exists
164 1
                if (! ($loweredTableName === strtolower($foreignKey->getForeignTableName()))) {
165 1
                    continue;
166
                }
167
168 1
                $myFk    = $foreignKey;
169 1
                $otherFk = null;
170
171 1
                foreach ($manyTable->getForeignKeys() as $manyTableForeignKey) {
172 1
                    if ($manyTableForeignKey !== $myFk) {
173
                        $otherFk = $manyTableForeignKey;
174
175 1
                        break;
176
                    }
177
                }
178
179 1
                if (! $otherFk) {
180
                    // the definition of this many to many table does not contain
181
                    // enough foreign key information to continue reverse engineering.
182 1
                    continue;
183
                }
184
185
                $localColumn = current($myFk->getColumns());
186
187
                $associationMapping                 = [];
188
                $associationMapping['fieldName']    = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getColumns()), true);
189
                $associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName());
190
191
                if (current($manyTable->getColumns())->getName() === $localColumn) {
192
                    $associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true);
193
                    $associationMapping['joinTable']  = new Mapping\JoinTableMetadata();
194
195
                    $joinTable = $associationMapping['joinTable'];
196
                    $joinTable->setName(strtolower($manyTable->getName()));
197
198
                    $fkCols = $myFk->getForeignColumns();
199
                    $cols   = $myFk->getColumns();
200
201
                    for ($i = 0, $l = count($cols); $i < $l; $i++) {
202
                        $joinColumn = new Mapping\JoinColumnMetadata();
203
204
                        $joinColumn->setColumnName($cols[$i]);
205
                        $joinColumn->setReferencedColumnName($fkCols[$i]);
206
207
                        $joinTable->addJoinColumn($joinColumn);
208
                    }
209
210
                    $fkCols = $otherFk->getForeignColumns();
211
                    $cols   = $otherFk->getColumns();
212
213
                    for ($i = 0, $l = count($cols); $i < $l; $i++) {
214
                        $joinColumn = new Mapping\JoinColumnMetadata();
215
216
                        $joinColumn->setColumnName($cols[$i]);
217
                        $joinColumn->setReferencedColumnName($fkCols[$i]);
218
219
                        $joinTable->addInverseJoinColumn($joinColumn);
220
                    }
221
                } else {
222
                    $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true);
223
                }
224
225
                $metadata->addProperty($associationMapping);
226
227 1
                break;
228
            }
229
        }
230 2
    }
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)) {
333
                continue;
334
            }
335
336 2
            $fieldName     = $this->getFieldNameForColumn($tableName, $column->getName(), false);
337 2
            $fieldMetadata = $this->convertColumnAnnotationToFieldMetadata($tableName, $column, $fieldName);
338
339 2
            if ($primaryKeys && in_array($column->getName(), $primaryKeys)) {
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
            $ids[0]->setValueGenerator(new Mapping\ValueGeneratorMetadata(Mapping\GeneratorType::AUTO));
351
        }
352 2
    }
353
354
    /**
355
     * Parse the given Column as FieldMetadata
356
     *
357
     * @return Mapping\FieldMetadata
358
     */
359 2
    private function convertColumnAnnotationToFieldMetadata(string $tableName, Column $column, string $fieldName)
360
    {
361 2
        $options       = [];
362 2
        $fieldMetadata = new Mapping\FieldMetadata($fieldName);
363
364 2
        $fieldMetadata->setType($column->getType());
365 2
        $fieldMetadata->setTableName($tableName);
366 2
        $fieldMetadata->setColumnName($column->getName());
367
368
        // Type specific elements
369 2
        switch ($column->getType()->getName()) {
370
            case Type::TARRAY:
371
            case Type::BLOB:
372
            case Type::GUID:
373
            case Type::JSON_ARRAY:
374
            case Type::OBJECT:
375
            case Type::SIMPLE_ARRAY:
376
            case Type::STRING:
377
            case Type::TEXT:
378 1
                if ($column->getLength()) {
379
                    $fieldMetadata->setLength($column->getLength());
380
                }
381
382 1
                $options['fixed'] = $column->getFixed();
383 1
                break;
384
385
            case Type::DECIMAL:
386
            case Type::FLOAT:
387
                $fieldMetadata->setScale($column->getScale());
388
                $fieldMetadata->setPrecision($column->getPrecision());
389
                break;
390
391
            case Type::INTEGER:
392
            case Type::BIGINT:
393
            case Type::SMALLINT:
394 2
                $options['unsigned'] = $column->getUnsigned();
395 2
                break;
396
        }
397
398
        // Comment
399 2
        $comment = $column->getComment();
400 2
        if ($comment !== null) {
401
            $options['comment'] = $comment;
402
        }
403
404
        // Default
405 2
        $default = $column->getDefault();
406 2
        if ($default !== null) {
407
            $options['default'] = $default;
408
        }
409
410 2
        $fieldMetadata->setOptions($options);
411
412 2
        return $fieldMetadata;
413
    }
414
415
    /**
416
     * Build to one (one to one, many to one) association mapping from class metadata.
417
     */
418 2
    private function buildToOneAssociationMappings(Mapping\ClassMetadata $metadata)
419
    {
420 2
        $tableName   = $metadata->getTableName();
421 2
        $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]);
422 2
        $foreignKeys = $this->getTableForeignKeys($this->tables[$tableName]);
423
424 2
        foreach ($foreignKeys as $foreignKey) {
425
            $foreignTableName   = $foreignKey->getForeignTableName();
426
            $fkColumns          = $foreignKey->getColumns();
427
            $fkForeignColumns   = $foreignKey->getForeignColumns();
428
            $localColumn        = current($fkColumns);
429
            $associationMapping = [
430
                'fieldName'    => $this->getFieldNameForColumn($tableName, $localColumn, true),
431
                'targetEntity' => $this->getClassNameForTable($foreignTableName),
432
            ];
433
434
            if ($metadata->getProperty($associationMapping['fieldName'])) {
435
                $associationMapping['fieldName'] .= '2'; // "foo" => "foo2"
436
            }
437
438
            if ($primaryKeys && in_array($localColumn, $primaryKeys)) {
439
                $associationMapping['id'] = true;
440
            }
441
442
            for ($i = 0, $l = count($fkColumns); $i < $l; $i++) {
443
                $joinColumn = new Mapping\JoinColumnMetadata();
444
445
                $joinColumn->setColumnName($fkColumns[$i]);
446
                $joinColumn->setReferencedColumnName($fkForeignColumns[$i]);
447
448
                $associationMapping['joinColumns'][] = $joinColumn;
449
            }
450
451
            // Here we need to check if $fkColumns are the same as $primaryKeys
452
            if (! array_diff($fkColumns, $primaryKeys)) {
453
                $metadata->addProperty($associationMapping);
454
            } else {
455
                $metadata->addProperty($associationMapping);
456
            }
457
        }
458 2
    }
459
460
    /**
461
     * Retrieve schema table definition foreign keys.
462
     *
463
     * @return ForeignKeyConstraint[]
464
     */
465 2
    private function getTableForeignKeys(Table $table)
466
    {
467 2
        return ($this->sm->getDatabasePlatform()->supportsForeignKeyConstraints())
468
            ? $table->getForeignKeys()
469 2
            : [];
470
    }
471
472
    /**
473
     * Retrieve schema table definition primary keys.
474
     *
475
     * @return Identifier[]
476
     */
477 2
    private function getTablePrimaryKeys(Table $table)
478
    {
479
        try {
480 2
            return $table->getPrimaryKey()->getColumns();
481
        } catch (SchemaException $e) {
482
            // Do nothing
483
        }
484
485
        return [];
486
    }
487
488
    /**
489
     * Returns the mapped class name for a table if it exists. Otherwise return "classified" version.
490
     *
491
     * @param string $tableName
492
     *
493
     * @return string
494
     */
495 2
    private function getClassNameForTable($tableName)
496
    {
497 2
        return $this->namespace . (
498 2
            $this->classNamesForTables[$tableName]
0 ignored issues
show
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

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