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;
}
}
@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