Defining Discriminator Maps at Child-level in Doctrine 2

Jasper Kuperus
7 min readMay 11, 2014

--

Doctrine ORM is a very powerful object-relation mapper for PHP 5.3.0+ which provides us with several powerful features. One of these features is the ability to define mappings using annotations in your model classes.

When using inheritance, there are two types, Single Table Inheritance and Class Table Inheritance, for which you need to help Doctrine a little bit by defining the possible subclasses. For these inheritance types you need to specify a discriminator map using the @DiscriminatorMap annotation, which looks something like this:

/**
* @Entity
* @InheritanceType( "SINGLE_TABLE" )
* @DiscriminatorColumn( name = "discr", type = "string" )
* @DiscriminatorMap( { "person" = "Person",
* "employee" = "Employee" } )
*/
class Person {
// Implementation…
}
/**
* @Entity
*/
class Employee extends Person {
// Implementation…
}

Using such inheritance with a discriminator map introduces the need to update the parent class whenever you create a subclass. Wait. What!? That just isn’t right. Apart from mixed up responsibilities, in some situations you just can’t modify your parent class. Let’s fix this.

Annotations

First of all, let’s stop using the @DiscriminatorMap annotation. Instead, let’s introduce the annotation @DiscriminatorEntry. This annotation provides only the discriminator value for the subclass it’s being used in, this includes the parent class.

/**
* @Entity
* @InheritanceType( "SINGLE_TABLE" )
* @DiscriminatorColumn( name = "discr", type = "string" )
* @DiscriminatorEntry( value = "person" )
*/
class Person {
// Implementation…
}
/**
* @Entity
* @DiscriminatorEntry( value = "employee" )
*/
class Employee extends Person {
// Implementation…
}

Now, how do we read annotations? Luckily, the AnnotationReader of Doctrine already does this for us. Below, we define a short class that helps us reading the annotations and another for the annotation.

Annotation::$reader = new DoctrineCommonAnnotationsAnnotationReader();
Annotation::$reader->setDefaultAnnotationNamespace( __NAMESPACE__ . "" );
class Annotation {
public static $reader;
public static function getAnnotationsForClass( $className ) {
$class = new ReflectionClass( $className );
return Annotation::$reader->getClassAnnotations( $class );
}
}
class DiscriminatorEntry {
private $value;
public function __construct( array $data ) {
$this->value = $data['value'];
}
public function getValue() {
return $this->value;
}
}

Note: You will probably want a bit more validation on the annotation. Here we simply set the value and leave out such validation. So in our case, every annotation that at least contains an attribute named value will do the job.

Doctrine Events

Another great feature of Doctrine 2.0 is the Event System. This lets us hook into Doctrine in an elegant way. We define an EventSubscriber that subscribes itself to the loadClassMetadata event. The loadClassMetadata event is fired after the metadata for a class has been loaded. When we get this event, we build the discriminator map and provide it to Doctrine. The following code shows the skeleton used for this:

class DiscriminatorListener implements Doctrine\Common\EventSubscriber {
private $driver; // Doctrines Metadata Driver
private $map; // Our temporary map for calculations
private $cachedMap; // The cached map for fast lookups
const ENTRY_ANNOTATION = 'Namespace\To\The\DiscriminatorEntry'; public function getSubscribedEvents() {
return Array( Doctrine\ORM\Events::loadClassMetadata );
}
public function __construct( Doctrine\ORM\EntityManager $db ) {
$this->driver = $db->getConfiguration()
->getMetadataDriverImpl();
$this->cachedMap = Array();
}
public function loadClassMetadata( Doctrine\ORM\Event\LoadClassMetadataEventArgs $event ) {
// Respond to the event, implementation provided later…
}
}

This self-written Event Listener should now be handed over to Doctrine, so it can dispatch events to it. This is done in the following way:

$em = Doctrine\ORM\EntityManager::create( $connectionOptions, $config );
$em->getEventManager()->addEventSubscriber( new Namespace\To\The\DiscriminatorListener( $em ) );

Implementing the DiscriminatorListener

We have defined several helper methods in the DiscriminatorListener. In this section we discuss all these helper methods and finally the method for handling the loadClassMetadata event.

Note: When interpreting the @DiscriminatorMap annotation, Doctrine automatically distills the subclasses and the discriminator value for the parent class out of this map. It is very important for us to also set these values. This does however only apply for the top-most parent class.

Extracting Discriminator Entries

The method extractEntry() retrieves all the annotations for the class and looks for the @DiscriminatorEntry annotation. When this is found, it is submitted in the map if this is a unique value. It returns a boolean that tells us whether the annotation was found or not.

private function extractEntry( $class ) {
$annotations = Namespace\To\Annotation::getAnnotationForClass( $class );
$success = false;
if( array_key_exists( self::ENTRY_ANNOTATION, $annotations ) ) {
$value = $annotations[self::ENTRY_ANNOTATION]->getValue();
if( in_array( $value, $this->map ) ) {
throw new Exception( "Found duplicate discriminator map entry '" . $value . "' in " . $class );
}
$this->map[$class] = $value;
$success = true;
}
return $success;
}

Building the Discriminator Map

For building the discriminator map, we use the method checkFamily(). This method checks whether the requested class is the top-most parent or not. If this is not the case, we do a recursive call with the parent class in order to also check all classes that extend that class. Otherwise, we use the method checkChildren() which looks for classes that extend the given class and have a discriminator value. We only have to check the children if we are the top-most parent, since checkChildren() recursively checks the children for every child again. In a nutshell, this method constructs the map out of the inheritance tree.

private function checkFamily( $class ) {
$rc = new ReflectionClass( $class );
$parent = $rc->getParentClass()->name;
if( $parent !== false ) {
// Also check all the children of our parent
$this->checkFamily( $parent );
} else {
// This is the top-most parent, used in overrideMetadata
$this->cachedMap[$class]['isParent'] = true;
// Find all the children of this class
$this->checkChildren( $class );
}
}
private function checkChildren( $class ) {
foreach( $this->driver->getAllClassNames() as $name ) {
$cRc = new ReflectionClass( $name );
$cParent = $cRc->getParentClass()->name;
// Haven't done this class yet? Go for it.
if( ! array_key_exists( $name, $this->map )
&& $cParent == $class && $this->extractEntry( $name ) ) {
$this->checkChildren( $name );
}
}
}

Overriding metadata

For overriding the metadata, we need to set the discriminator map and the discriminator value. If we have the top-most parent, we should also define the subclasses of this class. The subclasses are easily calculated by removing itself from the map. For this we use the cachedMap, which is built in the loadClassMetadata() method. The following keys apply for this array:

  • map: The discriminator map
  • discr: The discriminator value
  • isParent: Is this the top-most parent?

The cachedMap has for every class that was identified an entry that contains an array with the above keys. The overrideMetadata() then looks as follows:

private function overrideMetadata( Doctrine\ORM\Event\LoadClassMetadataEventArgs $event, $class ) {
// Set the discriminator map and value
$event->getClassMetadata()->discriminatorMap =
$this->cachedMap[$class]['map'];
$event->getClassMetadata()->discriminatorValue =
$this->cachedMap[$class]['discr'];
// If we are the top-most parent, set subclasses!
if( isset( $this->cachedMap[$class]['isParent'] )
&& $this->cachedMap[$class]['isParent'] === true ) {
$subclasses = $this->cachedMap[$class]['map'];
unset( $subclasses[$this->cachedMap[$class]['discr']] );
$event->getClassMetadata()->subClasses =
array_values( $subclasses );
}
}

Note: The event has to be used here to override the metadata. When not using the event, you be aware that for methods of Doctrine that again generate the loadClassMetadata event.

Handling the event

Then, finally, we will handle the event. First we will do a look-up to see whether we already calculated the map to which this class belongs, we can then simply override the metadata directly. Otherwise, a check is done whether we have an empty discriminator map and whether the @DiscriminatorEntry annotation was found. When this is found, we have to build the whole map, using checkFamily(). If this is not the case, there is simply nothing to do for this class.

After calculating the map, which is then an array with as keys the classnames and as values the discriminator values, we should build the cachedMap for fast look-ups in the future. For this we first flip the map to comply to the format Doctrine uses and per class we save this and its discriminator value in the cachedMap. Finally, the data of the class where it all started with is overridden using the overrideMetadata() method:

public function loadClassMetadata( Doctrine\ORM\Event\LoadClassMetadataEventArgs $event ) {
// Reset the temporary calculation map and get the classname
$this->map = Array();
$class = $event->getClassMetadata()->name;
// Did we already calculate the map for this element?
if( array_key_exists( $class, $this->cachedMap ) ) {
$this->overrideMetadata( $event, $class );
return;
}
// Do we have to process this class?
if( count( $event->getClassMetadata()->discriminatorMap ) ==
&& $this->extractEntry( $class ) ) {
// Now build the whole map
$this->checkFamily( $class );
} else {
// Nothing to do…
return;
}
// Create the lookup entries
$dMap = array_flip( $this->map );
foreach( $this->map as $cName => $discr ) {
$this->cachedMap[$cName]['map'] = $dMap;
$this->cachedMap[$cName]['discr'] = $this->map[$cName];
}
// Override the data for this class
$this->overrideMetadata( $event, $class );
}

Conclusions & Performance

When putting all the pieces together, we can now define discriminator map entries at the child level and work our way around the ugly way in which Doctrine handles inheritance using discriminator maps. There is however a big side-note to this. This will be an intensive calculation and doing it for every request would not be a smart thing to do. Although it is not discussed in this article, it is a good practice to (mem)cache the entries that are produced in the cachedMap.

Another more efficient way might be to incorporate the calculation of the discriminator maps in the run script which you will probably use to let Doctrine create the database scheme. By building the cache there, the DiscriminatorListener will only remain one task, the look-up in the cache.

Here’s a gist that gathers all code shown in this article. Enjoy!

--

--