Skip to content

Instantly share code, notes, and snippets.

@BacLuc
Last active March 2, 2021 20:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BacLuc/4476549b47a470b86f5d1fa84157c824 to your computer and use it in GitHub Desktop.
Save BacLuc/4476549b47a470b86f5d1fa84157c824 to your computer and use it in GitHub Desktop.
<?php
/**
* Created by PhpStorm.
* User: lucius
* Date: 19.10.16
* Time: 16:49
*/
namespace Concrete\Package\BasicTablePackage\Src\DiscriminatorEntry;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\SimpleAnnotationReader;
/**
* Class Annotation
* @package Concrete\Package\BasicTablePackage\Src\DiscriminatorEntry
*/
class Annotation
{
/**
* @var SimpleAnnotationReader
*/
public static $reader;
public static function getAnnotationsForClass( $className ) {
$class = new \ReflectionClass( $className );
return Annotation::$reader->getClassAnnotations( $class );
}
}
//set the reader. Externally, because you cannot set a static property as a return value of a function
Annotation::$reader = new AnnotationReader();
//the function with the default namespace does not exist anymore
//Annotation::$reader->setDefaultAnnotationNamespace( __NAMESPACE__ . "" );
<?php
/**
* Created by PhpStorm.
* User: lucius
* Date: 19.10.16
* Time: 16:48
*/
namespace Concrete\Package\BasicTablePackage\Src\DiscriminatorEntry;
/**
* Class DiscriminatorEntry
* package Concrete\Package\BasicTablePackage\Src\DiscriminatorEntry
* added Target Annotation that it only should have effect on class annotations
* if you want to use an attribute, you need to define it, and the type, else you have an exception
* @Annotation
* @Target({"CLASS"})
* @Attributes({
* @Attribute("value", type = "string"),
* })
*/
class DiscriminatorEntry
{
private $value;
public function __construct( array $data ) {
$this->value = $data['value'];
}
public function getValue() {
return $this->value;
}
}
<?php
namespace Concrete\Package\BasicTablePackage\Src\DiscriminatorEntry;
use Concrete\Package\BasicTablePackage\Src\BaseEntity;
/**
* 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 = 'Concrete\Package\BasicTablePackage\Src\DiscriminatorEntry\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();
}
/**
*
* @param $class
* @return array
*/
public static function getSubClasses($class){
$subclasses = array();
foreach(get_declared_classes() as $potentialSubclass)
{
$reflection = new \ReflectionClass($potentialSubclass);
if($reflection ->isSubclassOf($class)){
$subclasses[] = $potentialSubclass;
}
}
return $subclasses;
}
public function loadClassMetadata( \Doctrine\ORM\Event\LoadClassMetadataEventArgs $event ) {
// Reset the temporary calculation map and get the classname
$this->map = Array();
$class = $event->getClassMetadata()->name;
//because it now has some problem with not importet annotations, limit it to the entities i have control over
$reflection = new \ReflectionClass($class);
if(!$reflection->isSubclassOf("Concrete\\Package\\BasicTablePackage\\Src\\BaseEntity")){
return;
}
// 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 ) < 2
&& $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']] );
if(!is_array($subclasses)){
$subclasses = array();
}
$event->getClassMetadata()->subClasses = array_values( $subclasses );
}
}
private function checkFamily( $class ) {
$rc = new \ReflectionClass( $class );
$parent = $rc->getParentClass()->name;
if( $parent !== null) { //if no parent class is there, its null, not 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 ) {
/*
Because $this->driver->getAllClassNames() did not work, implemented own method to get subclasses
attention, getSubClasses returns all Child Classes, not only the direct Child classes
*/
foreach( static::getSubClasses($class) as $name ) {
$cRc = new \ReflectionClass( $name );
$cParent = $cRc->getParentClass()->name;
// Haven't done this class yet? Go for it.
//removed the check if it is a direct child. It does not really matter (and didn't work somehow)
if( !array_key_exists( $name, $this->map ) && $this->extractEntry( $name ) ) {
$this->checkChildren( $name );
}
}
}
private function extractEntry( $class )
{
$annotations = Annotation::getAnnotationsForClass($class);
/*
* getAnnotationsForClass gives back an array like this. There is no key called class
* Array
(
[0] => Doctrine\ORM\Mapping\Entity Object
(
[repositoryClass] =>
[readOnly] =>
)
[1] => Concrete\Package\BasicTablePackage\Src\DiscriminatorEntry\DiscriminatorEntry Object
(
[value:Concrete\Package\BasicTablePackage\Src\DiscriminatorEntry\DiscriminatorEntry:private] => Concrete\Package\BaclucPersonPackage\Src\PostalAddress
)
[2] => Doctrine\ORM\Mapping\Table Object
(
[name] => bacluc_postal_address
[schema] =>
[indexes] =>
[uniqueConstraints] =>
[options] => Array
(
)
)
)
* */
$success = false;
foreach($annotations as $key => $annotation){
if(get_class($annotation) == self::ENTRY_ANNOTATION){
//TODO check for duplicates
$this->map[$class] = $annotation->getValue();
$success = true;
}
}
// if (is_array($annotations['class'])) {
// 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;
}
}
<?php
class Controller extends Package{
/**
* @param EntityManager $em
* Because Doctrine itself requires on the topmost entity a discriminatormap with all subentities,
* we add here a EventListener when Doctrine Parses the Annotations.
* This DiscriminatorListener scans the Annotations of Child Classes of BaseEntity for
* @DiscriminatorEntry(value="Namespace\Classname") and adds them to the Discriminator Map,
* So that you don't have to define the Cildren in the topmost parent class.
*/
public static function addDiscriminatorListenerToEm(EntityManager $em){
if(!$em->DiscriminatorListenerAttached) {
$em->getEventManager()->addEventSubscriber(new DiscriminatorListener($em));
$em->DiscriminatorListenerAttached = true;
}
}
/**
* @return EntityManager
* @overrides Package::getEntityManager
* if the Package is installed, this function calls static::addDiscriminatorListenerToEm on the EntityManager
* To add support for @DiscriminatorEntry Annotation
* Only after Installation, because else the Classes to Support this are not found
*/
public function getEntityManager()
{
$em = parent::getEntityManager(); // TODO: Change the autogenerated stub
if(parent::isPackageInstalled()) {
static::addDiscriminatorListenerToEm($em);
}
return $em;
}
?>
@BacLuc
Copy link
Author

BacLuc commented Oct 20, 2016

Tested https://gist.github.com/jasperkuperus/03302fefe6e4722ab650 in the environment of Concrete5 Packages.
Don't know if Doctrine changed so much between 2.x and 2.4 Concrete5 is using, but i had to make several changes.

@redusek
Copy link

redusek commented Jul 27, 2020

Nice. Thank you!

@xkzl
Copy link

xkzl commented Feb 22, 2021

Many thanks for the working example @BacLuc

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