Last active
July 7, 2023 17:35
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @Entity | |
* @InheritanceType( “SINGLE_TABLE” ) | |
* @DiscriminatorColumn( name = “discr”, type = “string” ) | |
* @DiscriminatorEntry( value = “person” ) | |
*/ | |
class Person { | |
// Implementation… | |
} | |
/** | |
* @Entity | |
* @DiscriminatorEntry( value = “employee” ) | |
*/ | |
class Employee extends Person { | |
// Implementation… | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Any chance of having a working version of with php 8 attributes?