Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
This gist shows you how to define your discriminator maps at child level in doctrine 2. Why? Because your parent class shouldn't be aware of all it's subclasses. Please read my article for more explanation: https://medium.com/@jasperkuperus/defining-discriminator-maps-at-child-level-in-doctrine-2-1cd2ded95ffb
<?php
namespace My\Namespace;
/**
* This Listener listens to the loadClassMetadata event. Upon this event
* it hooks into Doctrine to update discriminator maps. Adding entries
* to the discriminator map at parent level is just not nice. We turn this
* around with this mechanism. In the subclass you will be able to give an
* entry for the discriminator map. In this listener we will retrieve the
* load metadata event to update the parent with a good discriminator map,
* collecting all entries from the subclasses.
*/
class DiscriminatorListener implements \Doctrine\Common\EventSubscriber {
// The driver of Doctrine, can be used to find all loaded classes
private $driver;
// The *temporary* map used for one run, when computing everything
private $map;
// The cached map, this holds the results after a computation, also for other classes
private $cachedMap;
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 ) {
// 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 ) == 0
&& $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 );
}
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 );
}
}
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 );
}
}
}
private function extractEntry( $class ) {
$annotations = \Namespace\To\Annotation::getAnnotationForClass( $class );
$success = false;
if( array_key_exists( self::ENTRY_ANNOTATION, $annotations['class'] ) ) {
$value = $annotations['class'][self::ENTRY_ANNOTATION]->value;
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;
}
}
/**
* @Entity
* @InheritanceType( “SINGLE_TABLE” )
* @DiscriminatorColumn( name = “discr”, type = “string” )
* @DiscriminatorEntry( value = “person” )
*/
class Person {
// Implementation…
}
/**
* @Entity
* @DiscriminatorEntry( value = “employee” )
*/
class Employee extends Person {
// Implementation…
}
// Put this where you bootstrap your EntityManager
$em = Doctrine\ORM\EntityManager::create( $connectionOptions, $config );
$em->getEventManager()->addEventSubscriber( new Namespace\To\The\DiscriminatorListener( $em ) );
// Code below is for annotation definition
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;
}
}
@TomasVotruba

This comment has been minimized.

Copy link

commented Sep 30, 2015

Really great idea. Just looking for decoupling logic from parent abstract class.

@vladimirdjuricic

This comment has been minimized.

Copy link

commented Jun 4, 2016

Fantastic! Thx!

@BacLuc

This comment has been minimized.

Copy link

commented Oct 20, 2016

Tested it a little, with some adjustments it worked.
Thx!
https://gist.github.com/BacLuc/4476549b47a470b86f5d1fa84157c824

the idea should be implemented in Doctrine itself

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.