. */ namespace Doctrine\ORM\Mapping\Driver; use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver; use Doctrine\Common\Persistence\Mapping\ClassMetadata; use Doctrine\Common\Util\Inflector; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Types\Type; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Mapping\MappingException; /** * The DatabaseDriver reverse engineers the mapping metadata from a database. * * @link www.doctrine-project.org * @since 2.0 * @author Guilherme Blanco * @author Jonathan Wage * @author Benjamin Eberlei */ class DatabaseDriver implements MappingDriver { /** * @var AbstractSchemaManager */ private $_sm; /** * @var array|null */ private $tables = null; /** * @var array */ private $classToTableNames = array(); /** * @var array */ private $manyToManyTables = array(); /** * @var array */ private $classNamesForTables = array(); /** * @var array */ private $fieldNamesForColumns = array(); /** * The namespace for the generated entities. * * @var string|null */ private $namespace; /** * @param AbstractSchemaManager $schemaManager */ public function __construct(AbstractSchemaManager $schemaManager) { $this->_sm = $schemaManager; } /** * Set the namespace for the generated entities. * * @param string $namespace * * @return void */ public function setNamespace($namespace) { $this->namespace = $namespace; } /** * {@inheritDoc} */ public function isTransient($className) { return true; } /** * {@inheritDoc} */ public function getAllClassNames() { $this->reverseEngineerMappingFromDatabase(); return array_keys($this->classToTableNames); } /** * Sets class name for a table. * * @param string $tableName * @param string $className * * @return void */ public function setClassNameForTable($tableName, $className) { $this->classNamesForTables[$tableName] = $className; } /** * Sets field name for a column on a specific table. * * @param string $tableName * @param string $columnName * @param string $fieldName * * @return void */ public function setFieldNameForColumn($tableName, $columnName, $fieldName) { $this->fieldNamesForColumns[$tableName][$columnName] = $fieldName; } /** * Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager. * * @param array $entityTables * @param array $manyToManyTables * * @return void */ public function setTables($entityTables, $manyToManyTables) { $this->tables = $this->manyToManyTables = $this->classToTableNames = array(); 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; } } /** * {@inheritDoc} */ public function loadMetadataForClass($className, ClassMetadata $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->getColumns()); $associationMapping = array(); $associationMapping['fieldName'] = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getColumns()), true); $associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName()); if (current($manyTable->getColumns())->getName() == $localColumn) { $associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true); $associationMapping['joinTable'] = array( 'name' => strtolower($manyTable->getName()), 'joinColumns' => array(), 'inverseJoinColumns' => array(), ); $fkCols = $myFk->getForeignColumns(); $cols = $myFk->getColumns(); for ($i = 0; $i < count($cols); $i++) { $associationMapping['joinTable']['joinColumns'][] = array( 'name' => $cols[$i], 'referencedColumnName' => $fkCols[$i], ); } $fkCols = $otherFk->getForeignColumns(); $cols = $otherFk->getColumns(); for ($i = 0; $i < count($cols); $i++) { $associationMapping['joinTable']['inverseJoinColumns'][] = array( 'name' => $cols[$i], 'referencedColumnName' => $fkCols[$i], ); } } else { $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true); } $metadata->mapManyToMany($associationMapping); break; } } } /** * @return void * * @throws \Doctrine\ORM\Mapping\MappingException */ private function reverseEngineerMappingFromDatabase() { if ($this->tables !== null) { return; } $tables = array(); foreach ($this->_sm->listTableNames() as $tableName) { $tables[$tableName] = $this->_sm->listTableDetails($tableName); } $this->tables = $this->manyToManyTables = $this->classToTableNames = array(); foreach ($tables as $tableName => $table) { $foreignKeys = ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) ? $table->getForeignKeys() : array(); $allForeignKeyColumns = array(); foreach ($foreignKeys as $foreignKey) { $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns()); } if ( ! $table->hasPrimaryKey()) { throw new MappingException( "Table " . $table->getName() . " has no primary key. Doctrine does not ". "support reverse engineering from tables that don't have a primary key." ); } $pkColumns = $table->getPrimaryKey()->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. * * @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata */ private function buildIndexes(ClassMetadataInfo $metadata) { $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. * * @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata */ private function buildFieldMappings(ClassMetadataInfo $metadata) { $tableName = $metadata->table['name']; $columns = $this->tables[$tableName]->getColumns(); $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]); $foreignKeys = $this->getTableForeignKeys($this->tables[$tableName]); $allForeignKeys = array(); foreach ($foreignKeys as $foreignKey) { $allForeignKeys = array_merge($allForeignKeys, $foreignKey->getLocalColumns()); } $ids = array(); $fieldMappings = array(); foreach ($columns as $column) { if (in_array($column->getName(), $allForeignKeys)) { continue; } $fieldMapping = $this->buildFieldMapping($tableName, $column); if ($primaryKeys && in_array($column->getName(), $primaryKeys)) { $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(ClassMetadataInfo::GENERATOR_TYPE_AUTO); } foreach ($fieldMappings as $fieldMapping) { $metadata->mapField($fieldMapping); } } /** * Build field mapping from a schema column definition * * @param string $tableName * @param \Doctrine\DBAL\Schema\Column $column * * @return array */ private function buildFieldMapping($tableName, Column $column) { $fieldMapping = array( 'fieldName' => $this->getFieldNameForColumn($tableName, $column->getName(), false), 'columnName' => $column->getName(), 'type' => strtolower((string) $column->getType()), 'nullable' => ( ! $column->getNotNull()), ); // Type specific elements switch ($fieldMapping['type']) { case Type::TARRAY: case Type::BLOB: case Type::GUID: case Type::JSON_ARRAY: case Type::OBJECT: case Type::SIMPLE_ARRAY: case Type::STRING: case Type::TEXT: $fieldMapping['length'] = $column->getLength(); $fieldMapping['fixed'] = $column->getFixed(); break; case Type::DECIMAL: case Type::FLOAT: $fieldMapping['precision'] = $column->getPrecision(); $fieldMapping['scale'] = $column->getScale(); break; case Type::INTEGER: case Type::BIGINT: case Type::SMALLINT: $fieldMapping['unsigned'] = $column->getUnsigned(); break; } // Comment if (($comment = $column->getComment()) !== null) { $fieldMapping['comment'] = $comment; } // Default if (($default = $column->getDefault()) !== null) { $fieldMapping['default'] = $default; } return $fieldMapping; } /** * Build to one (one to one, many to one) association mapping from class metadata. * * @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata */ private function buildToOneAssociationMappings(ClassMetadataInfo $metadata) { $tableName = $metadata->table['name']; $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]); $foreignKeys = $this->getTableForeignKeys($this->tables[$tableName]); foreach ($foreignKeys as $foreignKey) { $foreignTableName = $foreignKey->getForeignTableName(); $fkColumns = $foreignKey->getColumns(); $fkForeignColumns = $foreignKey->getForeignColumns(); $localColumn = current($fkColumns); $associationMapping = array( '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)) { $associationMapping['id'] = true; } for ($i = 0; $i < count($fkColumns); $i++) { $associationMapping['joinColumns'][] = array( '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); } } } /** * Retreive schema table definition foreign keys. * * @param \Doctrine\DBAL\Schema\Table $table * * @return array */ private function getTableForeignKeys(Table $table) { return ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) ? $table->getForeignKeys() : array(); } /** * Retreive schema table definition primary keys. * * @param \Doctrine\DBAL\Schema\Table $table * * @return array */ private function getTablePrimaryKeys(Table $table) { try { return $table->getPrimaryKey()->getColumns(); } catch(SchemaException $e) { // Do nothing } return array(); } /** * Returns the mapped class name for a table if it exists. Otherwise return "classified" version. * * @param string $tableName * * @return string */ private function getClassNameForTable($tableName) { if (isset($this->classNamesForTables[$tableName])) { return $this->namespace . $this->classNamesForTables[$tableName]; } return $this->namespace . Inflector::classify(strtolower($tableName)); } /** * Return the mapped field name for a column, if it exists. Otherwise return camelized version. * * @param string $tableName * @param string $columnName * @param boolean $fk Whether the column is a foreignkey or not. * * @return string */ private function getFieldNameForColumn($tableName, $columnName, $fk = false) { if (isset($this->fieldNamesForColumns[$tableName]) && isset($this->fieldNamesForColumns[$tableName][$columnName])) { return $this->fieldNamesForColumns[$tableName][$columnName]; } $columnName = strtolower($columnName); // Replace _id if it is a foreignkey column if ($fk) { $columnName = str_replace('_id', '', $columnName); } return Inflector::camelize($columnName); } }