-
-
Save jasperkuperus/03302fefe6e4722ab650 to your computer and use it in GitHub Desktop.
<?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; | |
} | |
} |
Fantastic! Thx!
Tested it a little, with some adjustments it worked.
Thx!
https://gist.github.com/BacLuc/4476549b47a470b86f5d1fa84157c824
the idea should be implemented in Doctrine itself
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
Very smart! Many thanks @jasperkuperus
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.
Any chance of having a working version of with php 8 attributes?
Really great idea. Just looking for decoupling logic from parent abstract class.