summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php
diff options
context:
space:
mode:
authorpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
committerpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
commitbf6655a534a6775d30cafa67bd801276bda1d98d (patch)
treec6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php
parent94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff)
downloadAppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.zip
VERSION 0.2 doctrine ORM et entités
Diffstat (limited to 'vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php')
-rw-r--r--vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php528
1 files changed, 528 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php b/vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php
new file mode 100644
index 0000000..49e2e93
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php
@@ -0,0 +1,528 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Driver;
6
7use Doctrine\DBAL\Schema\AbstractSchemaManager;
8use Doctrine\DBAL\Schema\Column;
9use Doctrine\DBAL\Schema\SchemaException;
10use Doctrine\DBAL\Schema\Table;
11use Doctrine\DBAL\Types\Type;
12use Doctrine\DBAL\Types\Types;
13use Doctrine\Inflector\Inflector;
14use Doctrine\Inflector\InflectorFactory;
15use Doctrine\ORM\Mapping\ClassMetadata;
16use Doctrine\ORM\Mapping\MappingException;
17use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
18use Doctrine\Persistence\Mapping\Driver\MappingDriver;
19use InvalidArgumentException;
20use TypeError;
21
22use function array_diff;
23use function array_keys;
24use function array_merge;
25use function assert;
26use function count;
27use function current;
28use function get_debug_type;
29use function in_array;
30use function preg_replace;
31use function sort;
32use function sprintf;
33use function strtolower;
34
35/**
36 * The DatabaseDriver reverse engineers the mapping metadata from a database.
37 *
38 * @link www.doctrine-project.org
39 */
40class DatabaseDriver implements MappingDriver
41{
42 /**
43 * Replacement for {@see Types::ARRAY}.
44 *
45 * To be removed as soon as support for DBAL 3 is dropped.
46 */
47 private const ARRAY = 'array';
48
49 /**
50 * Replacement for {@see Types::OBJECT}.
51 *
52 * To be removed as soon as support for DBAL 3 is dropped.
53 */
54 private const OBJECT = 'object';
55
56 /** @var array<string,Table>|null */
57 private array|null $tables = null;
58
59 /** @var array<class-string, string> */
60 private array $classToTableNames = [];
61
62 /** @psalm-var array<string, Table> */
63 private array $manyToManyTables = [];
64
65 /** @var mixed[] */
66 private array $classNamesForTables = [];
67
68 /** @var mixed[] */
69 private array $fieldNamesForColumns = [];
70
71 /**
72 * The namespace for the generated entities.
73 */
74 private string|null $namespace = null;
75
76 private Inflector $inflector;
77
78 public function __construct(private readonly AbstractSchemaManager $sm)
79 {
80 $this->inflector = InflectorFactory::create()->build();
81 }
82
83 /**
84 * Set the namespace for the generated entities.
85 */
86 public function setNamespace(string $namespace): void
87 {
88 $this->namespace = $namespace;
89 }
90
91 public function isTransient(string $className): bool
92 {
93 return true;
94 }
95
96 /**
97 * {@inheritDoc}
98 */
99 public function getAllClassNames(): array
100 {
101 $this->reverseEngineerMappingFromDatabase();
102
103 return array_keys($this->classToTableNames);
104 }
105
106 /**
107 * Sets class name for a table.
108 */
109 public function setClassNameForTable(string $tableName, string $className): void
110 {
111 $this->classNamesForTables[$tableName] = $className;
112 }
113
114 /**
115 * Sets field name for a column on a specific table.
116 */
117 public function setFieldNameForColumn(string $tableName, string $columnName, string $fieldName): void
118 {
119 $this->fieldNamesForColumns[$tableName][$columnName] = $fieldName;
120 }
121
122 /**
123 * Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager.
124 *
125 * @param Table[] $entityTables
126 * @param Table[] $manyToManyTables
127 * @psalm-param list<Table> $entityTables
128 * @psalm-param list<Table> $manyToManyTables
129 */
130 public function setTables(array $entityTables, array $manyToManyTables): void
131 {
132 $this->tables = $this->manyToManyTables = $this->classToTableNames = [];
133
134 foreach ($entityTables as $table) {
135 $className = $this->getClassNameForTable($table->getName());
136
137 $this->classToTableNames[$className] = $table->getName();
138 $this->tables[$table->getName()] = $table;
139 }
140
141 foreach ($manyToManyTables as $table) {
142 $this->manyToManyTables[$table->getName()] = $table;
143 }
144 }
145
146 public function setInflector(Inflector $inflector): void
147 {
148 $this->inflector = $inflector;
149 }
150
151 /**
152 * {@inheritDoc}
153 *
154 * @psalm-param class-string<T> $className
155 * @psalm-param ClassMetadata<T> $metadata
156 *
157 * @template T of object
158 */
159 public function loadMetadataForClass(string $className, PersistenceClassMetadata $metadata): void
160 {
161 if (! $metadata instanceof ClassMetadata) {
162 throw new TypeError(sprintf(
163 'Argument #2 passed to %s() must be an instance of %s, %s given.',
164 __METHOD__,
165 ClassMetadata::class,
166 get_debug_type($metadata),
167 ));
168 }
169
170 $this->reverseEngineerMappingFromDatabase();
171
172 if (! isset($this->classToTableNames[$className])) {
173 throw new InvalidArgumentException('Unknown class ' . $className);
174 }
175
176 $tableName = $this->classToTableNames[$className];
177
178 $metadata->name = $className;
179 $metadata->table['name'] = $tableName;
180
181 $this->buildIndexes($metadata);
182 $this->buildFieldMappings($metadata);
183 $this->buildToOneAssociationMappings($metadata);
184
185 foreach ($this->manyToManyTables as $manyTable) {
186 foreach ($manyTable->getForeignKeys() as $foreignKey) {
187 // foreign key maps to the table of the current entity, many to many association probably exists
188 if (! (strtolower($tableName) === strtolower($foreignKey->getForeignTableName()))) {
189 continue;
190 }
191
192 $myFk = $foreignKey;
193 $otherFk = null;
194
195 foreach ($manyTable->getForeignKeys() as $foreignKey) {
196 if ($foreignKey !== $myFk) {
197 $otherFk = $foreignKey;
198 break;
199 }
200 }
201
202 if (! $otherFk) {
203 // the definition of this many to many table does not contain
204 // enough foreign key information to continue reverse engineering.
205 continue;
206 }
207
208 $localColumn = current($myFk->getLocalColumns());
209
210 $associationMapping = [];
211 $associationMapping['fieldName'] = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getLocalColumns()), true);
212 $associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName());
213
214 if (current($manyTable->getColumns())->getName() === $localColumn) {
215 $associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getLocalColumns()), true);
216 $associationMapping['joinTable'] = [
217 'name' => strtolower($manyTable->getName()),
218 'joinColumns' => [],
219 'inverseJoinColumns' => [],
220 ];
221
222 $fkCols = $myFk->getForeignColumns();
223 $cols = $myFk->getLocalColumns();
224
225 for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) {
226 $associationMapping['joinTable']['joinColumns'][] = [
227 'name' => $cols[$i],
228 'referencedColumnName' => $fkCols[$i],
229 ];
230 }
231
232 $fkCols = $otherFk->getForeignColumns();
233 $cols = $otherFk->getLocalColumns();
234
235 for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) {
236 $associationMapping['joinTable']['inverseJoinColumns'][] = [
237 'name' => $cols[$i],
238 'referencedColumnName' => $fkCols[$i],
239 ];
240 }
241 } else {
242 $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getLocalColumns()), true);
243 }
244
245 $metadata->mapManyToMany($associationMapping);
246
247 break;
248 }
249 }
250 }
251
252 /** @throws MappingException */
253 private function reverseEngineerMappingFromDatabase(): void
254 {
255 if ($this->tables !== null) {
256 return;
257 }
258
259 $this->tables = $this->manyToManyTables = $this->classToTableNames = [];
260
261 foreach ($this->sm->listTables() as $table) {
262 $tableName = $table->getName();
263 $foreignKeys = $table->getForeignKeys();
264
265 $allForeignKeyColumns = [];
266
267 foreach ($foreignKeys as $foreignKey) {
268 $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns());
269 }
270
271 $primaryKey = $table->getPrimaryKey();
272 if ($primaryKey === null) {
273 throw new MappingException(
274 'Table ' . $tableName . ' has no primary key. Doctrine does not ' .
275 "support reverse engineering from tables that don't have a primary key.",
276 );
277 }
278
279 $pkColumns = $primaryKey->getColumns();
280
281 sort($pkColumns);
282 sort($allForeignKeyColumns);
283
284 if ($pkColumns === $allForeignKeyColumns && count($foreignKeys) === 2) {
285 $this->manyToManyTables[$tableName] = $table;
286 } else {
287 // lower-casing is necessary because of Oracle Uppercase Tablenames,
288 // assumption is lower-case + underscore separated.
289 $className = $this->getClassNameForTable($tableName);
290
291 $this->tables[$tableName] = $table;
292 $this->classToTableNames[$className] = $tableName;
293 }
294 }
295 }
296
297 /**
298 * Build indexes from a class metadata.
299 */
300 private function buildIndexes(ClassMetadata $metadata): void
301 {
302 $tableName = $metadata->table['name'];
303 $indexes = $this->tables[$tableName]->getIndexes();
304
305 foreach ($indexes as $index) {
306 if ($index->isPrimary()) {
307 continue;
308 }
309
310 $indexName = $index->getName();
311 $indexColumns = $index->getColumns();
312 $constraintType = $index->isUnique()
313 ? 'uniqueConstraints'
314 : 'indexes';
315
316 $metadata->table[$constraintType][$indexName]['columns'] = $indexColumns;
317 }
318 }
319
320 /**
321 * Build field mapping from class metadata.
322 */
323 private function buildFieldMappings(ClassMetadata $metadata): void
324 {
325 $tableName = $metadata->table['name'];
326 $columns = $this->tables[$tableName]->getColumns();
327 $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]);
328 $foreignKeys = $this->tables[$tableName]->getForeignKeys();
329 $allForeignKeys = [];
330
331 foreach ($foreignKeys as $foreignKey) {
332 $allForeignKeys = array_merge($allForeignKeys, $foreignKey->getLocalColumns());
333 }
334
335 $ids = [];
336 $fieldMappings = [];
337
338 foreach ($columns as $column) {
339 if (in_array($column->getName(), $allForeignKeys, true)) {
340 continue;
341 }
342
343 $fieldMapping = $this->buildFieldMapping($tableName, $column);
344
345 if ($primaryKeys && in_array($column->getName(), $primaryKeys, true)) {
346 $fieldMapping['id'] = true;
347 $ids[] = $fieldMapping;
348 }
349
350 $fieldMappings[] = $fieldMapping;
351 }
352
353 // We need to check for the columns here, because we might have associations as id as well.
354 if ($ids && count($primaryKeys) === 1) {
355 $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO);
356 }
357
358 foreach ($fieldMappings as $fieldMapping) {
359 $metadata->mapField($fieldMapping);
360 }
361 }
362
363 /**
364 * Build field mapping from a schema column definition
365 *
366 * @return mixed[]
367 * @psalm-return array{
368 * fieldName: string,
369 * columnName: string,
370 * type: string,
371 * nullable: bool,
372 * options: array{
373 * unsigned?: bool,
374 * fixed?: bool,
375 * comment: string|null,
376 * default?: mixed
377 * },
378 * precision?: int,
379 * scale?: int,
380 * length?: int|null
381 * }
382 */
383 private function buildFieldMapping(string $tableName, Column $column): array
384 {
385 $fieldMapping = [
386 'fieldName' => $this->getFieldNameForColumn($tableName, $column->getName(), false),
387 'columnName' => $column->getName(),
388 'type' => Type::getTypeRegistry()->lookupName($column->getType()),
389 'nullable' => ! $column->getNotnull(),
390 'options' => [
391 'comment' => $column->getComment(),
392 ],
393 ];
394
395 // Type specific elements
396 switch ($fieldMapping['type']) {
397 case self::ARRAY:
398 case Types::BLOB:
399 case Types::GUID:
400 case self::OBJECT:
401 case Types::SIMPLE_ARRAY:
402 case Types::STRING:
403 case Types::TEXT:
404 $fieldMapping['length'] = $column->getLength();
405 $fieldMapping['options']['fixed'] = $column->getFixed();
406 break;
407
408 case Types::DECIMAL:
409 case Types::FLOAT:
410 $fieldMapping['precision'] = $column->getPrecision();
411 $fieldMapping['scale'] = $column->getScale();
412 break;
413
414 case Types::INTEGER:
415 case Types::BIGINT:
416 case Types::SMALLINT:
417 $fieldMapping['options']['unsigned'] = $column->getUnsigned();
418 break;
419 }
420
421 // Default
422 $default = $column->getDefault();
423 if ($default !== null) {
424 $fieldMapping['options']['default'] = $default;
425 }
426
427 return $fieldMapping;
428 }
429
430 /**
431 * Build to one (one to one, many to one) association mapping from class metadata.
432 */
433 private function buildToOneAssociationMappings(ClassMetadata $metadata): void
434 {
435 assert($this->tables !== null);
436
437 $tableName = $metadata->table['name'];
438 $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]);
439 $foreignKeys = $this->tables[$tableName]->getForeignKeys();
440
441 foreach ($foreignKeys as $foreignKey) {
442 $foreignTableName = $foreignKey->getForeignTableName();
443 $fkColumns = $foreignKey->getLocalColumns();
444 $fkForeignColumns = $foreignKey->getForeignColumns();
445 $localColumn = current($fkColumns);
446 $associationMapping = [
447 'fieldName' => $this->getFieldNameForColumn($tableName, $localColumn, true),
448 'targetEntity' => $this->getClassNameForTable($foreignTableName),
449 ];
450
451 if (isset($metadata->fieldMappings[$associationMapping['fieldName']])) {
452 $associationMapping['fieldName'] .= '2'; // "foo" => "foo2"
453 }
454
455 if ($primaryKeys && in_array($localColumn, $primaryKeys, true)) {
456 $associationMapping['id'] = true;
457 }
458
459 for ($i = 0, $fkColumnsCount = count($fkColumns); $i < $fkColumnsCount; $i++) {
460 $associationMapping['joinColumns'][] = [
461 'name' => $fkColumns[$i],
462 'referencedColumnName' => $fkForeignColumns[$i],
463 ];
464 }
465
466 // Here we need to check if $fkColumns are the same as $primaryKeys
467 if (! array_diff($fkColumns, $primaryKeys)) {
468 $metadata->mapOneToOne($associationMapping);
469 } else {
470 $metadata->mapManyToOne($associationMapping);
471 }
472 }
473 }
474
475 /**
476 * Retrieve schema table definition primary keys.
477 *
478 * @return string[]
479 */
480 private function getTablePrimaryKeys(Table $table): array
481 {
482 try {
483 return $table->getPrimaryKey()->getColumns();
484 } catch (SchemaException) {
485 // Do nothing
486 }
487
488 return [];
489 }
490
491 /**
492 * Returns the mapped class name for a table if it exists. Otherwise return "classified" version.
493 *
494 * @psalm-return class-string
495 */
496 private function getClassNameForTable(string $tableName): string
497 {
498 if (isset($this->classNamesForTables[$tableName])) {
499 return $this->namespace . $this->classNamesForTables[$tableName];
500 }
501
502 return $this->namespace . $this->inflector->classify(strtolower($tableName));
503 }
504
505 /**
506 * Return the mapped field name for a column, if it exists. Otherwise return camelized version.
507 *
508 * @param bool $fk Whether the column is a foreignkey or not.
509 */
510 private function getFieldNameForColumn(
511 string $tableName,
512 string $columnName,
513 bool $fk = false,
514 ): string {
515 if (isset($this->fieldNamesForColumns[$tableName], $this->fieldNamesForColumns[$tableName][$columnName])) {
516 return $this->fieldNamesForColumns[$tableName][$columnName];
517 }
518
519 $columnName = strtolower($columnName);
520
521 // Replace _id if it is a foreignkey column
522 if ($fk) {
523 $columnName = preg_replace('/_id$/', '', $columnName);
524 }
525
526 return $this->inflector->camelize($columnName);
527 }
528}