From bf6655a534a6775d30cafa67bd801276bda1d98d Mon Sep 17 00:00:00 2001 From: polo Date: Tue, 13 Aug 2024 23:45:21 +0200 Subject: =?UTF-8?q?VERSION=200.2=20doctrine=20ORM=20et=20entit=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orm/src/Mapping/Driver/DatabaseDriver.php | 528 +++++++++++++++++++++ 1 file changed, 528 insertions(+) create mode 100644 vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php (limited to 'vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php') 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 @@ +|null */ + private array|null $tables = null; + + /** @var array */ + private array $classToTableNames = []; + + /** @psalm-var array */ + private array $manyToManyTables = []; + + /** @var mixed[] */ + private array $classNamesForTables = []; + + /** @var mixed[] */ + private array $fieldNamesForColumns = []; + + /** + * The namespace for the generated entities. + */ + private string|null $namespace = null; + + private Inflector $inflector; + + public function __construct(private readonly AbstractSchemaManager $sm) + { + $this->inflector = InflectorFactory::create()->build(); + } + + /** + * Set the namespace for the generated entities. + */ + public function setNamespace(string $namespace): void + { + $this->namespace = $namespace; + } + + public function isTransient(string $className): bool + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getAllClassNames(): array + { + $this->reverseEngineerMappingFromDatabase(); + + return array_keys($this->classToTableNames); + } + + /** + * Sets class name for a table. + */ + public function setClassNameForTable(string $tableName, string $className): void + { + $this->classNamesForTables[$tableName] = $className; + } + + /** + * Sets field name for a column on a specific table. + */ + public function setFieldNameForColumn(string $tableName, string $columnName, string $fieldName): void + { + $this->fieldNamesForColumns[$tableName][$columnName] = $fieldName; + } + + /** + * Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager. + * + * @param Table[] $entityTables + * @param Table[] $manyToManyTables + * @psalm-param list $entityTables + * @psalm-param list
$manyToManyTables + */ + public function setTables(array $entityTables, array $manyToManyTables): void + { + $this->tables = $this->manyToManyTables = $this->classToTableNames = []; + + foreach ($entityTables as $table) { + $className = $this->getClassNameForTable($table->getName()); + + $this->classToTableNames[$className] = $table->getName(); + $this->tables[$table->getName()] = $table; + } + + foreach ($manyToManyTables as $table) { + $this->manyToManyTables[$table->getName()] = $table; + } + } + + public function setInflector(Inflector $inflector): void + { + $this->inflector = $inflector; + } + + /** + * {@inheritDoc} + * + * @psalm-param class-string $className + * @psalm-param ClassMetadata $metadata + * + * @template T of object + */ + public function loadMetadataForClass(string $className, PersistenceClassMetadata $metadata): void + { + if (! $metadata instanceof ClassMetadata) { + throw new TypeError(sprintf( + 'Argument #2 passed to %s() must be an instance of %s, %s given.', + __METHOD__, + ClassMetadata::class, + get_debug_type($metadata), + )); + } + + $this->reverseEngineerMappingFromDatabase(); + + if (! isset($this->classToTableNames[$className])) { + throw new InvalidArgumentException('Unknown class ' . $className); + } + + $tableName = $this->classToTableNames[$className]; + + $metadata->name = $className; + $metadata->table['name'] = $tableName; + + $this->buildIndexes($metadata); + $this->buildFieldMappings($metadata); + $this->buildToOneAssociationMappings($metadata); + + foreach ($this->manyToManyTables as $manyTable) { + foreach ($manyTable->getForeignKeys() as $foreignKey) { + // foreign key maps to the table of the current entity, many to many association probably exists + if (! (strtolower($tableName) === strtolower($foreignKey->getForeignTableName()))) { + continue; + } + + $myFk = $foreignKey; + $otherFk = null; + + foreach ($manyTable->getForeignKeys() as $foreignKey) { + if ($foreignKey !== $myFk) { + $otherFk = $foreignKey; + break; + } + } + + if (! $otherFk) { + // the definition of this many to many table does not contain + // enough foreign key information to continue reverse engineering. + continue; + } + + $localColumn = current($myFk->getLocalColumns()); + + $associationMapping = []; + $associationMapping['fieldName'] = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getLocalColumns()), true); + $associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName()); + + if (current($manyTable->getColumns())->getName() === $localColumn) { + $associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getLocalColumns()), true); + $associationMapping['joinTable'] = [ + 'name' => strtolower($manyTable->getName()), + 'joinColumns' => [], + 'inverseJoinColumns' => [], + ]; + + $fkCols = $myFk->getForeignColumns(); + $cols = $myFk->getLocalColumns(); + + for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) { + $associationMapping['joinTable']['joinColumns'][] = [ + 'name' => $cols[$i], + 'referencedColumnName' => $fkCols[$i], + ]; + } + + $fkCols = $otherFk->getForeignColumns(); + $cols = $otherFk->getLocalColumns(); + + for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) { + $associationMapping['joinTable']['inverseJoinColumns'][] = [ + 'name' => $cols[$i], + 'referencedColumnName' => $fkCols[$i], + ]; + } + } else { + $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getLocalColumns()), true); + } + + $metadata->mapManyToMany($associationMapping); + + break; + } + } + } + + /** @throws MappingException */ + private function reverseEngineerMappingFromDatabase(): void + { + if ($this->tables !== null) { + return; + } + + $this->tables = $this->manyToManyTables = $this->classToTableNames = []; + + foreach ($this->sm->listTables() as $table) { + $tableName = $table->getName(); + $foreignKeys = $table->getForeignKeys(); + + $allForeignKeyColumns = []; + + foreach ($foreignKeys as $foreignKey) { + $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns()); + } + + $primaryKey = $table->getPrimaryKey(); + if ($primaryKey === null) { + throw new MappingException( + 'Table ' . $tableName . ' has no primary key. Doctrine does not ' . + "support reverse engineering from tables that don't have a primary key.", + ); + } + + $pkColumns = $primaryKey->getColumns(); + + sort($pkColumns); + sort($allForeignKeyColumns); + + if ($pkColumns === $allForeignKeyColumns && count($foreignKeys) === 2) { + $this->manyToManyTables[$tableName] = $table; + } else { + // lower-casing is necessary because of Oracle Uppercase Tablenames, + // assumption is lower-case + underscore separated. + $className = $this->getClassNameForTable($tableName); + + $this->tables[$tableName] = $table; + $this->classToTableNames[$className] = $tableName; + } + } + } + + /** + * Build indexes from a class metadata. + */ + private function buildIndexes(ClassMetadata $metadata): void + { + $tableName = $metadata->table['name']; + $indexes = $this->tables[$tableName]->getIndexes(); + + foreach ($indexes as $index) { + if ($index->isPrimary()) { + continue; + } + + $indexName = $index->getName(); + $indexColumns = $index->getColumns(); + $constraintType = $index->isUnique() + ? 'uniqueConstraints' + : 'indexes'; + + $metadata->table[$constraintType][$indexName]['columns'] = $indexColumns; + } + } + + /** + * Build field mapping from class metadata. + */ + private function buildFieldMappings(ClassMetadata $metadata): void + { + $tableName = $metadata->table['name']; + $columns = $this->tables[$tableName]->getColumns(); + $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]); + $foreignKeys = $this->tables[$tableName]->getForeignKeys(); + $allForeignKeys = []; + + foreach ($foreignKeys as $foreignKey) { + $allForeignKeys = array_merge($allForeignKeys, $foreignKey->getLocalColumns()); + } + + $ids = []; + $fieldMappings = []; + + foreach ($columns as $column) { + if (in_array($column->getName(), $allForeignKeys, true)) { + continue; + } + + $fieldMapping = $this->buildFieldMapping($tableName, $column); + + if ($primaryKeys && in_array($column->getName(), $primaryKeys, true)) { + $fieldMapping['id'] = true; + $ids[] = $fieldMapping; + } + + $fieldMappings[] = $fieldMapping; + } + + // We need to check for the columns here, because we might have associations as id as well. + if ($ids && count($primaryKeys) === 1) { + $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO); + } + + foreach ($fieldMappings as $fieldMapping) { + $metadata->mapField($fieldMapping); + } + } + + /** + * Build field mapping from a schema column definition + * + * @return mixed[] + * @psalm-return array{ + * fieldName: string, + * columnName: string, + * type: string, + * nullable: bool, + * options: array{ + * unsigned?: bool, + * fixed?: bool, + * comment: string|null, + * default?: mixed + * }, + * precision?: int, + * scale?: int, + * length?: int|null + * } + */ + private function buildFieldMapping(string $tableName, Column $column): array + { + $fieldMapping = [ + 'fieldName' => $this->getFieldNameForColumn($tableName, $column->getName(), false), + 'columnName' => $column->getName(), + 'type' => Type::getTypeRegistry()->lookupName($column->getType()), + 'nullable' => ! $column->getNotnull(), + 'options' => [ + 'comment' => $column->getComment(), + ], + ]; + + // Type specific elements + switch ($fieldMapping['type']) { + case self::ARRAY: + case Types::BLOB: + case Types::GUID: + case self::OBJECT: + case Types::SIMPLE_ARRAY: + case Types::STRING: + case Types::TEXT: + $fieldMapping['length'] = $column->getLength(); + $fieldMapping['options']['fixed'] = $column->getFixed(); + break; + + case Types::DECIMAL: + case Types::FLOAT: + $fieldMapping['precision'] = $column->getPrecision(); + $fieldMapping['scale'] = $column->getScale(); + break; + + case Types::INTEGER: + case Types::BIGINT: + case Types::SMALLINT: + $fieldMapping['options']['unsigned'] = $column->getUnsigned(); + break; + } + + // Default + $default = $column->getDefault(); + if ($default !== null) { + $fieldMapping['options']['default'] = $default; + } + + return $fieldMapping; + } + + /** + * Build to one (one to one, many to one) association mapping from class metadata. + */ + private function buildToOneAssociationMappings(ClassMetadata $metadata): void + { + assert($this->tables !== null); + + $tableName = $metadata->table['name']; + $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]); + $foreignKeys = $this->tables[$tableName]->getForeignKeys(); + + foreach ($foreignKeys as $foreignKey) { + $foreignTableName = $foreignKey->getForeignTableName(); + $fkColumns = $foreignKey->getLocalColumns(); + $fkForeignColumns = $foreignKey->getForeignColumns(); + $localColumn = current($fkColumns); + $associationMapping = [ + 'fieldName' => $this->getFieldNameForColumn($tableName, $localColumn, true), + 'targetEntity' => $this->getClassNameForTable($foreignTableName), + ]; + + if (isset($metadata->fieldMappings[$associationMapping['fieldName']])) { + $associationMapping['fieldName'] .= '2'; // "foo" => "foo2" + } + + if ($primaryKeys && in_array($localColumn, $primaryKeys, true)) { + $associationMapping['id'] = true; + } + + for ($i = 0, $fkColumnsCount = count($fkColumns); $i < $fkColumnsCount; $i++) { + $associationMapping['joinColumns'][] = [ + 'name' => $fkColumns[$i], + 'referencedColumnName' => $fkForeignColumns[$i], + ]; + } + + // Here we need to check if $fkColumns are the same as $primaryKeys + if (! array_diff($fkColumns, $primaryKeys)) { + $metadata->mapOneToOne($associationMapping); + } else { + $metadata->mapManyToOne($associationMapping); + } + } + } + + /** + * Retrieve schema table definition primary keys. + * + * @return string[] + */ + private function getTablePrimaryKeys(Table $table): array + { + try { + return $table->getPrimaryKey()->getColumns(); + } catch (SchemaException) { + // Do nothing + } + + return []; + } + + /** + * Returns the mapped class name for a table if it exists. Otherwise return "classified" version. + * + * @psalm-return class-string + */ + private function getClassNameForTable(string $tableName): string + { + if (isset($this->classNamesForTables[$tableName])) { + return $this->namespace . $this->classNamesForTables[$tableName]; + } + + return $this->namespace . $this->inflector->classify(strtolower($tableName)); + } + + /** + * Return the mapped field name for a column, if it exists. Otherwise return camelized version. + * + * @param bool $fk Whether the column is a foreignkey or not. + */ + private function getFieldNameForColumn( + string $tableName, + string $columnName, + bool $fk = false, + ): string { + if (isset($this->fieldNamesForColumns[$tableName], $this->fieldNamesForColumns[$tableName][$columnName])) { + return $this->fieldNamesForColumns[$tableName][$columnName]; + } + + $columnName = strtolower($columnName); + + // Replace _id if it is a foreignkey column + if ($fk) { + $columnName = preg_replace('/_id$/', '', $columnName); + } + + return $this->inflector->camelize($columnName); + } +} -- cgit v1.2.3