diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index befc2973c..dde634d9d 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -340,6 +340,10 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface $class->setParentClasses($visited); + if ( $class->isRootEntity() && ! $class->isInheritanceTypeNone() && ! $class->discriminatorMap) { + $this->addDefaultDiscriminatorMap($class); + } + if ($this->evm->hasListeners(Events::loadClassMetadata)) { $eventArgs = new \Doctrine\ORM\Event\LoadClassMetadataEventArgs($class, $this->em); $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs); @@ -410,6 +414,95 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface return new ClassMetadata($className, $this->em->getConfiguration()->getNamingStrategy()); } + /** + * Adds a default discriminator map if no one is given + * + * If an entity is of any inheritance type and does not contain a + * discrimiator map, then the map is generated automatically. This process + * is expensive computation wise. + * + * The automatically generated discriminator map contains the lowercase shortname of + * each class as key. + * + * @param \Doctrine\ORM\Mapping\ClassMetadata $class + */ + private function addDefaultDiscriminatorMap(ClassMetadata $class) + { + $allClasses = $this->driver->getAllClassNames(); + $subClassesMetadata = array(); + $fqcn = $class->getName(); + $map = array($this->getShortName($class->name) => $fqcn); + + $duplicates = array(); + foreach ($allClasses as $subClassCandidate) { + if (is_subclass_of($subClassCandidate, $fqcn)) { + $shortName = $this->getShortName($subClassCandidate); + + if (isset($map[$shortName])) { + $duplicates[] = $shortName; + } + + $map[$shortName] = $subClassCandidate; + } + } + + if ($duplicates) { + throw MappingException::duplicateDiscriminatorEntry($class->name, $duplicates, $map); + } + + $class->setDiscriminatorMap($map); + } + + /** + * Get the lower-case shortname of a class. + * + * @param string $className + * @return string + */ + private function getShortName($className) + { + if (strpos($className, "\\") === false) { + return strtolower($className); + } + + $parts = explode("\\", $className); + return strtolower(end($parts)); + } + + /** + * Cache the metadata + * + * @param $className + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata + */ + private function cacheMetadata($className, ClassMetadata $metadata) + { + $this->cacheDriver->save( + "$className\$CLASSMETADATA", $metadata, null + ); + } + + /** + * Verify if metadata is cached + * + * @param $className + * @return bool + */ + private function cacheContainsMetadata($className) + { + return $this->cacheDriver->contains("$className\$CLASSMETADATA"); + } + + /** + * Fetch metadata from cache + * + * @param $className + */ + private function fetchMetadataFromCache($className) + { + return $this->cacheDriver->fetch("$className\$CLASSMETADATA"); + } + /** * Adds inherited fields to the subclass mapping. * diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index d34bb31ae..d8894923e 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -1898,6 +1898,16 @@ class ClassMetadataInfo implements ClassMetadata return isset($this->fieldMappings[$fieldName]['inherited']); } + /** + * Check if this entity is the root in any entity-inheritance-hierachy. + * + * @return bool + */ + public function isRootEntity() + { + return $this->name == $this->rootEntityName; + } + /** * Checks whether a mapped association field is inherited from a superclass. * diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index be4604c1b..f0f7b0ed9 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -298,6 +298,17 @@ class MappingException extends \Doctrine\ORM\ORMException ); } + public static function duplicateDiscriminatorEntry($className, array $entries, array $map) + { + return new self( + "The entries " . implode(', ', $entries) . " in discriminator map of class '" . $className . "' is duplicated. " . + "If the discriminator map is automatically generated you have to convert it to an explicit discriminator map now. " . + "The entries of the current map are: @DiscriminatorMap({" . implode(', ', array_map( + function($a, $b) { return "'$a': '$b'"; }, array_keys($map), array_values($map) + )) . "})" + ); + } + public static function missingDiscriminatorMap($className) { return new self("Entity class '$className' is using inheritance but no discriminator map was defined."); diff --git a/tests/Doctrine/Tests/Models/JoinedInheritanceType/AnotherChildClass.php b/tests/Doctrine/Tests/Models/JoinedInheritanceType/AnotherChildClass.php new file mode 100644 index 000000000..e49d89807 --- /dev/null +++ b/tests/Doctrine/Tests/Models/JoinedInheritanceType/AnotherChildClass.php @@ -0,0 +1,10 @@ +assertFalse($em->getMetadataFactory()->isTransient('CMS:CmsArticle')); } + public function testAddDefaultDiscriminatorMap() + { + $cmf = new ClassMetadataFactory(); + $driver = $this->createAnnotationDriver(array(__DIR__ . '/../../Models/JoinedInheritanceType/')); + $em = $this->_createEntityManager($driver); + $cmf->setEntityManager($em); + + $rootMetadata = $cmf->getMetadataFor('Doctrine\Tests\Models\JoinedInheritanceType\RootClass'); + $childMetadata = $cmf->getMetadataFor('Doctrine\Tests\Models\JoinedInheritanceType\ChildClass'); + $anotherChildMetadata = $cmf->getMetadataFor('Doctrine\Tests\Models\JoinedInheritanceType\AnotherChildClass'); + $rootDiscriminatorMap = $rootMetadata->discriminatorMap; + $childDiscriminatorMap = $childMetadata->discriminatorMap; + $anotherChildDiscriminatorMap = $anotherChildMetadata->discriminatorMap; + + $rootClass = 'Doctrine\Tests\Models\JoinedInheritanceType\RootClass'; + $childClass = 'Doctrine\Tests\Models\JoinedInheritanceType\ChildClass'; + $anotherChildClass = 'Doctrine\Tests\Models\JoinedInheritanceType\AnotherChildClass'; + + $rootClassKey = array_search($rootClass, $rootDiscriminatorMap); + $childClassKey = array_search($childClass, $rootDiscriminatorMap); + $anotherChildClassKey = array_search($anotherChildClass, $rootDiscriminatorMap); + + $this->assertEquals('rootclass', $rootClassKey); + $this->assertEquals('childclass', $childClassKey); + $this->assertEquals('anotherchildclass', $anotherChildClassKey); + + $this->assertEquals($childDiscriminatorMap, $rootDiscriminatorMap); + $this->assertEquals($anotherChildDiscriminatorMap, $rootDiscriminatorMap); + + // ClassMetadataFactory::addDefaultDiscriminatorMap shouldn't be called again, because the + // discriminator map is already cached + $cmf = $this->getMock('Doctrine\ORM\Mapping\ClassMetadataFactory', array('addDefaultDiscriminatorMap')); + $cmf->setEntityManager($em); + $cmf->expects($this->never()) + ->method('addDefaultDiscriminatorMap'); + + $rootMetadata = $cmf->getMetadataFor('Doctrine\Tests\Models\JoinedInheritanceType\RootClass'); + } + protected function _createEntityManager($metadataDriver) { $driverMock = new DriverMock();