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
Bug
introduced
by
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 |