-
-
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.