3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15 * This software consists of voluntary contributions made by many individuals
16 * and is licensed under the MIT license. For more information, see
17 * <http://www.doctrine-project.org>.
20 namespace Doctrine\ORM\Mapping\Driver;
22 use Doctrine\DBAL\Schema\AbstractSchemaManager,
23 Doctrine\DBAL\Schema\SchemaException,
24 Doctrine\Common\Persistence\Mapping\Driver\MappingDriver,
25 Doctrine\Common\Persistence\Mapping\ClassMetadata,
26 Doctrine\ORM\Mapping\ClassMetadataInfo,
27 Doctrine\Common\Util\Inflector,
28 Doctrine\ORM\Mapping\MappingException;
31 * The DatabaseDriver reverse engineers the mapping metadata from a database.
34 * @link www.doctrine-project.org
36 * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
37 * @author Jonathan Wage <jonwage@gmail.com>
38 * @author Benjamin Eberlei <kontakt@beberlei.de>
40 class DatabaseDriver implements MappingDriver
43 * @var AbstractSchemaManager
50 private $tables = null;
52 private $classToTableNames = array();
57 private $manyToManyTables = array();
62 private $classNamesForTables = array();
67 private $fieldNamesForColumns = array();
70 * The namespace for the generated entities.
78 * @param AbstractSchemaManager $schemaManager
80 public function __construct(AbstractSchemaManager $schemaManager)
82 $this->_sm = $schemaManager;
86 * Set tables manually instead of relying on the reverse engeneering capabilities of SchemaManager.
88 * @param array $entityTables
89 * @param array $manyToManyTables
92 public function setTables($entityTables, $manyToManyTables)
94 $this->tables = $this->manyToManyTables = $this->classToTableNames = array();
95 foreach ($entityTables as $table) {
96 $className = $this->getClassNameForTable($table->getName());
97 $this->classToTableNames[$className] = $table->getName();
98 $this->tables[$table->getName()] = $table;
100 foreach ($manyToManyTables as $table) {
101 $this->manyToManyTables[$table->getName()] = $table;
105 private function reverseEngineerMappingFromDatabase()
107 if ($this->tables !== null) {
113 foreach ($this->_sm->listTableNames() as $tableName) {
114 $tables[$tableName] = $this->_sm->listTableDetails($tableName);
117 $this->tables = $this->manyToManyTables = $this->classToTableNames = array();
118 foreach ($tables as $tableName => $table) {
119 /* @var $table \Doctrine\DBAL\Schema\Table */
120 if ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) {
121 $foreignKeys = $table->getForeignKeys();
123 $foreignKeys = array();
126 $allForeignKeyColumns = array();
127 foreach ($foreignKeys as $foreignKey) {
128 $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns());
131 if ( ! $table->hasPrimaryKey()) {
132 throw new MappingException(
133 "Table " . $table->getName() . " has no primary key. Doctrine does not ".
134 "support reverse engineering from tables that don't have a primary key."
138 $pkColumns = $table->getPrimaryKey()->getColumns();
140 sort($allForeignKeyColumns);
142 if ($pkColumns == $allForeignKeyColumns && count($foreignKeys) == 2) {
143 $this->manyToManyTables[$tableName] = $table;
145 // lower-casing is necessary because of Oracle Uppercase Tablenames,
146 // assumption is lower-case + underscore separated.
147 $className = $this->getClassNameForTable($tableName);
148 $this->tables[$tableName] = $table;
149 $this->classToTableNames[$className] = $tableName;
157 public function loadMetadataForClass($className, ClassMetadata $metadata)
159 $this->reverseEngineerMappingFromDatabase();
161 if (!isset($this->classToTableNames[$className])) {
162 throw new \InvalidArgumentException("Unknown class " . $className);
165 $tableName = $this->classToTableNames[$className];
167 $metadata->name = $className;
168 $metadata->table['name'] = $tableName;
170 $columns = $this->tables[$tableName]->getColumns();
171 $indexes = $this->tables[$tableName]->getIndexes();
173 $primaryKeyColumns = $this->tables[$tableName]->getPrimaryKey()->getColumns();
174 } catch(SchemaException $e) {
175 $primaryKeyColumns = array();
178 if ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) {
179 $foreignKeys = $this->tables[$tableName]->getForeignKeys();
181 $foreignKeys = array();
184 $allForeignKeyColumns = array();
185 foreach ($foreignKeys as $foreignKey) {
186 $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns());
190 $fieldMappings = array();
191 foreach ($columns as $column) {
192 $fieldMapping = array();
194 if (in_array($column->getName(), $allForeignKeyColumns)) {
196 } else if ($primaryKeyColumns && in_array($column->getName(), $primaryKeyColumns)) {
197 $fieldMapping['id'] = true;
200 $fieldMapping['fieldName'] = $this->getFieldNameForColumn($tableName, $column->getName(), false);
201 $fieldMapping['columnName'] = $column->getName();
202 $fieldMapping['type'] = strtolower((string) $column->getType());
204 if ($column->getType() instanceof \Doctrine\DBAL\Types\StringType) {
205 $fieldMapping['length'] = $column->getLength();
206 $fieldMapping['fixed'] = $column->getFixed();
207 } else if ($column->getType() instanceof \Doctrine\DBAL\Types\IntegerType) {
208 $fieldMapping['unsigned'] = $column->getUnsigned();
210 $fieldMapping['nullable'] = $column->getNotNull() ? false : true;
212 if (isset($fieldMapping['id'])) {
213 $ids[] = $fieldMapping;
215 $fieldMappings[] = $fieldMapping;
220 if (count($ids) == 1) {
221 $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO);
224 foreach ($ids as $id) {
225 $metadata->mapField($id);
229 foreach ($fieldMappings as $fieldMapping) {
230 $metadata->mapField($fieldMapping);
233 foreach ($this->manyToManyTables as $manyTable) {
234 foreach ($manyTable->getForeignKeys() as $foreignKey) {
235 // foreign key maps to the table of the current entity, many to many association probably exists
236 if (strtolower($tableName) == strtolower($foreignKey->getForeignTableName())) {
239 foreach ($manyTable->getForeignKeys() as $foreignKey) {
240 if ($foreignKey != $myFk) {
241 $otherFk = $foreignKey;
247 // the definition of this many to many table does not contain
248 // enough foreign key information to continue reverse engeneering.
252 $localColumn = current($myFk->getColumns());
253 $associationMapping = array();
254 $associationMapping['fieldName'] = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getColumns()), true);
255 $associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName());
256 if (current($manyTable->getColumns())->getName() == $localColumn) {
257 $associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true);
258 $associationMapping['joinTable'] = array(
259 'name' => strtolower($manyTable->getName()),
260 'joinColumns' => array(),
261 'inverseJoinColumns' => array(),
264 $fkCols = $myFk->getForeignColumns();
265 $cols = $myFk->getColumns();
266 for ($i = 0; $i < count($cols); $i++) {
267 $associationMapping['joinTable']['joinColumns'][] = array(
269 'referencedColumnName' => $fkCols[$i],
273 $fkCols = $otherFk->getForeignColumns();
274 $cols = $otherFk->getColumns();
275 for ($i = 0; $i < count($cols); $i++) {
276 $associationMapping['joinTable']['inverseJoinColumns'][] = array(
278 'referencedColumnName' => $fkCols[$i],
282 $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true);
284 $metadata->mapManyToMany($associationMapping);
290 foreach ($foreignKeys as $foreignKey) {
291 $foreignTable = $foreignKey->getForeignTableName();
292 $cols = $foreignKey->getColumns();
293 $fkCols = $foreignKey->getForeignColumns();
295 $localColumn = current($cols);
296 $associationMapping = array();
297 $associationMapping['fieldName'] = $this->getFieldNameForColumn($tableName, $localColumn, true);
298 $associationMapping['targetEntity'] = $this->getClassNameForTable($foreignTable);
300 if ($primaryKeyColumns && in_array($localColumn, $primaryKeyColumns)) {
301 $associationMapping['id'] = true;
304 for ($i = 0; $i < count($cols); $i++) {
305 $associationMapping['joinColumns'][] = array(
307 'referencedColumnName' => $fkCols[$i],
311 //Here we need to check if $cols are the same as $primaryKeyColums
312 if (!array_diff($cols,$primaryKeyColumns)) {
313 $metadata->mapOneToOne($associationMapping);
315 $metadata->mapManyToOne($associationMapping);
323 public function isTransient($className)
331 public function getAllClassNames()
333 $this->reverseEngineerMappingFromDatabase();
335 return array_keys($this->classToTableNames);
339 * Set class name for a table.
341 * @param string $tableName
342 * @param string $className
345 public function setClassNameForTable($tableName, $className)
347 $this->classNamesForTables[$tableName] = $className;
351 * Set field name for a column on a specific table.
353 * @param string $tableName
354 * @param string $columnName
355 * @param string $fieldName
358 public function setFieldNameForColumn($tableName, $columnName, $fieldName)
360 $this->fieldNamesForColumns[$tableName][$columnName] = $fieldName;
364 * Return the mapped class name for a table if it exists. Otherwise return "classified" version.
366 * @param string $tableName
369 private function getClassNameForTable($tableName)
371 if (isset($this->classNamesForTables[$tableName])) {
372 return $this->namespace . $this->classNamesForTables[$tableName];
375 return $this->namespace . Inflector::classify(strtolower($tableName));
379 * Return the mapped field name for a column, if it exists. Otherwise return camelized version.
381 * @param string $tableName
382 * @param string $columnName
383 * @param boolean $fk Whether the column is a foreignkey or not.
386 private function getFieldNameForColumn($tableName, $columnName, $fk = false)
388 if (isset($this->fieldNamesForColumns[$tableName]) && isset($this->fieldNamesForColumns[$tableName][$columnName])) {
389 return $this->fieldNamesForColumns[$tableName][$columnName];
392 $columnName = strtolower($columnName);
394 // Replace _id if it is a foreignkey column
396 $columnName = str_replace('_id', '', $columnName);
398 return Inflector::camelize($columnName);
402 * Set the namespace for the generated entities.
404 * @param string $namespace
407 public function setNamespace($namespace)
409 $this->namespace = $namespace;