Skip to content

Instantly share code, notes, and snippets.

@jasperkuperus
Last active July 7, 2023 17:35
Show Gist options
  • Save jasperkuperus/03302fefe6e4722ab650 to your computer and use it in GitHub Desktop.
Save jasperkuperus/03302fefe6e4722ab650 to your computer and use it in GitHub Desktop.
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
Copy link

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

Copy link

ghost commented Jun 4, 2016

Fantastic! Thx!

@BacLuc
Copy link

BacLuc 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

@redusek
Copy link

redusek commented Jul 27, 2020

Agreed. Doctrine should adopt this approach. Making the base class aware of its subclasses is not good practice (violates open-closed principle). Thank you @jasperkuperus

@meiyasan
Copy link

meiyasan commented Feb 22, 2021

Very smart! Many thanks @jasperkuperus

@jimbo2150
Copy link

Agreed. Doctrine should adopt this approach. Making the base class aware of its subclasses is not good practice (violates open-closed principle). Thank you @jasperkuperus

Agree, but on top of that... if you want to allow others to include their own types, they would have to modify your higher-level class to get the new types to function properly? That is promoting poor coding.

@Eldhelion
Copy link

Any chance of having a working version of with php 8 attributes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment