diff --git a/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php b/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php index 007be9639..9a12bbc72 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php @@ -19,12 +19,15 @@ namespace Doctrine\ORM\Mapping\Driver; -use Doctrine\DBAL\Schema\AbstractSchemaManager; -use Doctrine\DBAL\Schema\SchemaException; use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver; use Doctrine\Common\Persistence\Mapping\ClassMetadata; -use Doctrine\ORM\Mapping\ClassMetadataInfo; 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; /** @@ -84,249 +87,15 @@ class DatabaseDriver implements MappingDriver } /** - * Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager. + * Set the namespace for the generated entities. * - * @param array $entityTables - * @param array $manyToManyTables + * @param string $namespace * * @return void */ - public function setTables($entityTables, $manyToManyTables) + public function setNamespace($namespace) { - $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; - } - } - - /** - * @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) { - /* @var $table \Doctrine\DBAL\Schema\Table */ - if ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) { - $foreignKeys = $table->getForeignKeys(); - } else { - $foreignKeys = 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; - } - } - } - - /** - * {@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; - - $columns = $this->tables[$tableName]->getColumns(); - $indexes = $this->tables[$tableName]->getIndexes(); - try { - $primaryKeyColumns = $this->tables[$tableName]->getPrimaryKey()->getColumns(); - } catch(SchemaException $e) { - $primaryKeyColumns = array(); - } - - if ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) { - $foreignKeys = $this->tables[$tableName]->getForeignKeys(); - } else { - $foreignKeys = array(); - } - - $allForeignKeyColumns = array(); - foreach ($foreignKeys as $foreignKey) { - $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns()); - } - - $ids = array(); - $fieldMappings = array(); - foreach ($columns as $column) { - $fieldMapping = array(); - - if (in_array($column->getName(), $allForeignKeyColumns)) { - continue; - } else if ($primaryKeyColumns && in_array($column->getName(), $primaryKeyColumns)) { - $fieldMapping['id'] = true; - } - - $fieldMapping['fieldName'] = $this->getFieldNameForColumn($tableName, $column->getName(), false); - $fieldMapping['columnName'] = $column->getName(); - $fieldMapping['type'] = strtolower((string) $column->getType()); - - if ($column->getType() instanceof \Doctrine\DBAL\Types\StringType) { - $fieldMapping['length'] = $column->getLength(); - $fieldMapping['fixed'] = $column->getFixed(); - } else if ($column->getType() instanceof \Doctrine\DBAL\Types\IntegerType) { - $fieldMapping['unsigned'] = $column->getUnsigned(); - } - $fieldMapping['nullable'] = $column->getNotNull() ? false : true; - - if (isset($fieldMapping['id'])) { - $ids[] = $fieldMapping; - } else { - $fieldMappings[] = $fieldMapping; - } - } - - if ($ids) { - // We need to check for the columns here, because we might have associations as id as well. - if (count($primaryKeyColumns) == 1) { - $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO); - } - - foreach ($ids as $id) { - $metadata->mapField($id); - } - } - - foreach ($fieldMappings as $fieldMapping) { - $metadata->mapField($fieldMapping); - } - - 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())) { - $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; - } - } - } - - foreach ($foreignKeys as $foreignKey) { - $foreignTable = $foreignKey->getForeignTableName(); - $cols = $foreignKey->getColumns(); - $fkCols = $foreignKey->getForeignColumns(); - - $localColumn = current($cols); - $associationMapping = array(); - $associationMapping['fieldName'] = $this->getFieldNameForColumn($tableName, $localColumn, true); - $associationMapping['targetEntity'] = $this->getClassNameForTable($foreignTable); - - if (isset($metadata->fieldMappings[$associationMapping['fieldName']])) { - $associationMapping['fieldName'] = $associationMapping['fieldName'] . "2"; - } - - if ($primaryKeyColumns && in_array($localColumn, $primaryKeyColumns)) { - $associationMapping['id'] = true; - } - - for ($i = 0; $i < count($cols); $i++) { - $associationMapping['joinColumns'][] = array( - 'name' => $cols[$i], - 'referencedColumnName' => $fkCols[$i], - ); - } - - //Here we need to check if $cols are the same as $primaryKeyColumns - if (!array_diff($cols,$primaryKeyColumns)) { - $metadata->mapOneToOne($associationMapping); - } else { - $metadata->mapManyToOne($associationMapping); - } - } + $this->namespace = $namespace; } /** @@ -374,6 +143,376 @@ class DatabaseDriver implements MappingDriver $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. * @@ -413,16 +552,4 @@ class DatabaseDriver implements MappingDriver } return Inflector::camelize($columnName); } - - /** - * Set the namespace for the generated entities. - * - * @param string $namespace - * - * @return void - */ - public function setNamespace($namespace) - { - $this->namespace = $namespace; - } } diff --git a/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php b/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php index 0c30a0bef..b9039e714 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php @@ -145,4 +145,57 @@ class DatabaseDriverTest extends DatabaseDriverTestCase $this->assertEquals(0, count($metadatas['DbdriverBaz']->associationMappings), "no association mappings should be detected."); } + + public function testLoadMetadataFromDatabaseDetail() + { + if (!$this->_em->getConnection()->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $this->markTestSkipped('Platform does not support foreign keys.'); + } + + $table = new \Doctrine\DBAL\Schema\Table("dbdriver_foo"); + $table->addColumn('id', 'integer',array('unsigned'=>true)); + $table->setPrimaryKey(array('id')); + $table->addColumn('column_unsigned','integer',array('unsigned'=>true)); + $table->addColumn('column_comment','string',array('comment'=>'test_comment')); + $table->addColumn('column_default','string',array('default'=>'test_default')); + $table->addColumn('column_decimal','decimal',array('precision'=>4,'scale'=>3)); + + $table->addColumn('column_index1','string'); + $table->addColumn('column_index2','string'); + $table->addIndex(array('column_index1','column_index2'),'index1'); + + $table->addColumn('column_unique_index1','string'); + $table->addColumn('column_unique_index2','string'); + $table->addUniqueIndex(array('column_unique_index1','column_unique_index2'),'unique_index1'); + $this->_sm->dropAndCreateTable($table); + + $metadatas = $this->extractClassMetadata(array("DbdriverFoo")); + + $this->assertArrayHasKey('DbdriverFoo', $metadatas); + $metadata = $metadatas['DbdriverFoo']; + + $this->assertArrayHasKey('id', $metadata->fieldMappings); + $this->assertEquals('id', $metadata->fieldMappings['id']['fieldName']); + $this->assertEquals('id', strtolower($metadata->fieldMappings['id']['columnName'])); + $this->assertEquals('integer', (string)$metadata->fieldMappings['id']['type']); + + $this->assertArrayHasKey('columnUnsigned', $metadata->fieldMappings); + $this->assertTrue($metadata->fieldMappings['columnUnsigned']['unsigned']); + + $this->assertArrayHasKey('columnComment', $metadata->fieldMappings); + $this->assertEquals('test_comment', $metadata->fieldMappings['columnComment']['comment']); + + $this->assertArrayHasKey('columnDefault', $metadata->fieldMappings); + $this->assertEquals('test_default', $metadata->fieldMappings['columnDefault']['default']); + + $this->assertArrayHasKey('columnDecimal', $metadata->fieldMappings); + $this->assertEquals(4, $metadata->fieldMappings['columnDecimal']['precision']); + $this->assertEquals(3, $metadata->fieldMappings['columnDecimal']['scale']); + + $this->assertTrue(!empty($metadata->table['indexes']['index1']['columns'])); + $this->assertEquals(array('column_index1','column_index2'),$metadata->table['indexes']['index1']['columns']); + + $this->assertTrue(!empty($metadata->table['uniqueConstraints']['unique_index1']['columns'])); + $this->assertEquals(array('column_unique_index1','column_unique_index2'),$metadata->table['uniqueConstraints']['unique_index1']['columns']); + } }