Defining Discriminator Maps at Child-level in Doctrine 2

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

Annotations

/**
* @Entity
* @InheritanceType( "SINGLE_TABLE" )
* @DiscriminatorColumn( name = "discr", type = "string" )
* @DiscriminatorEntry( value = "person" )
*/
class Person {
// Implementation…
}
/**
* @Entity
* @DiscriminatorEntry( value = "employee" )
*/
class Employee extends Person {
// Implementation…
}
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;
}
}

Doctrine Events

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…
}
}
$em = Doctrine\ORM\EntityManager::create( $connectionOptions, $config );
$em->getEventManager()->addEventSubscriber( new Namespace\To\The\DiscriminatorListener( $em ) );

Implementing the DiscriminatorListener

Extracting Discriminator Entries

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

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

  • map: The discriminator map
  • discr: The discriminator value
  • isParent: Is this the top-most parent?
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 );
}
}

Handling the event

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

--

--

--

Tech enthusiast

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Jasper Kuperus

Jasper Kuperus

Tech enthusiast

More from Medium

The Crumbling Economy

Blockchain Gaming and NFT’s — The New Technology That is Going to Change Gaming

Squishmallows

Summary of the book “Chatter”