From 368cf73f892e266fce695bb3e2ab8a75aa603bc9 Mon Sep 17 00:00:00 2001 From: "Fabio B. Silva" Date: Sun, 29 Jul 2012 19:10:33 -0300 Subject: [PATCH] entity listeners mapping --- .../ORM/Mapping/ClassMetadataFactory.php | 6 +- .../ORM/Mapping/ClassMetadataInfo.php | 48 +++++++ .../ORM/Mapping/Driver/AnnotationDriver.php | 122 +++++++++++------- .../Mapping/Driver/DoctrineAnnotations.php | 1 + lib/Doctrine/ORM/Mapping/EntityListeners.php | 41 ++++++ .../Tests/Models/Company/CompanyContract.php | 1 + .../Models/Company/ContractSubscriber.php | 31 +++++ .../EntityListenersDispatcherTest.php | 43 ++++++ 8 files changed, 247 insertions(+), 46 deletions(-) create mode 100644 lib/Doctrine/ORM/Mapping/EntityListeners.php create mode 100644 tests/Doctrine/Tests/Models/Company/ContractSubscriber.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/EntityListenersDispatcherTest.php diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index a259efa84..718105748 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -156,6 +156,10 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory $this->addInheritedSqlResultSetMappings($class, $parent); } + if ($parent && !empty($parent->entityListeners) && empty($class->entityListeners)) { + $class->entityListeners = $parent->entityListeners; + } + $class->setParentClasses($nonSuperclassParents); if ( $class->isRootEntity() && ! $class->isInheritanceTypeNone() && ! $class->discriminatorMap) { @@ -411,7 +415,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory } } - /** + /** * Completes the ID generator mapping. If "auto" is specified we choose the generator * most appropriate for the targeted database platform. * diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index a9dfa9b8c..db4acbb8b 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -26,6 +26,7 @@ use Doctrine\DBAL\Types\Type; use ReflectionClass; use Doctrine\Common\Persistence\Mapping\ClassMetadata; use Doctrine\Common\ClassLoader; +use Doctrine\Common\EventArgs; /** * A ClassMetadata instance holds all the object-relational mapping metadata @@ -436,6 +437,18 @@ class ClassMetadataInfo implements ClassMetadata */ public $lifecycleCallbacks = array(); + /** + * READ-ONLY: The registered entity listeners. + * + * @var array + */ + public $entityListeners = array(); + + /** + * @var array entity listeners instances. + */ + static private $entityListenerInstances = array(); + /** * READ-ONLY: The association mappings of this class. * @@ -2492,6 +2505,41 @@ class ClassMetadataInfo implements ClassMetadata $this->lifecycleCallbacks = $callbacks; } + /** + * Adds a entity listener for entities of this class. + * + * @param string $callback + * @param string $eventName + */ + public function addEntityListener($eventName, $class, $method) + { + $this->entityListeners[$eventName][] = array( + 'class' => $class, + 'method' => $method + ); + } + + /** + * Call the entity listeners. + * + * @param string $eventName The event name. + * @param object $entity An instance of the mapped entity + * @param \Doctrine\Common\EventArgs $arg The Event args + */ + public function dispatchEntityListeners($eventName, $entity, EventArgs $arg) + { + foreach ($this->entityListeners[$eventName] as $listener) { + $class = $listener['class']; + $method = $listener['method']; + + if ( ! isset(self::$entityListenerInstances[$class])) { + self::$entityListenerInstances[$class] = new $class(); + } + + self::$entityListenerInstances[$class]->{$method}($entity, $arg); + } + } + /** * Sets the discriminator column definition. * diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 004b55c5b..f8abdd149 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -25,6 +25,7 @@ use Doctrine\ORM\Mapping\JoinColumn; use Doctrine\ORM\Mapping\Column; use Doctrine\Common\Persistence\Mapping\ClassMetadata; use Doctrine\Common\Persistence\Mapping\Driver\AnnotationDriver as AbstractAnnotationDriver; +use Doctrine\ORM\Events; /** * The AnnotationDriver reads the mapping metadata from docblock annotations. @@ -415,54 +416,39 @@ class AnnotationDriver extends AbstractAnnotationDriver } } + // Evaluate EntityListeners annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\EntityListeners'])) { + $entityListenersAnnot = $classAnnotations['Doctrine\ORM\Mapping\EntityListeners']; + + foreach ($entityListenersAnnot->value as $listener) { + + if ( ! class_exists($listener)) { + throw new \InvalidArgumentException("Indefined class \"$listener\""); + } + + $listener = new \ReflectionClass($listener); + + foreach ($listener->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + foreach ($this->getMethodCallbacks($method) as $value) { + list($callback, $event) = $value; + $metadata->addEntityListener($event, $listener->name, $callback); + } + } + } + } + // Evaluate @HasLifecycleCallbacks annotation if (isset($classAnnotations['Doctrine\ORM\Mapping\HasLifecycleCallbacks'])) { /* @var $method \ReflectionMethod */ - foreach ($class->getMethods() as $method) { + foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { // filter for the declaring class only, callbacks from parents will already be registered. - if ($method->isPublic() && $method->getDeclaringClass()->getName() == $class->name) { - $annotations = $this->reader->getMethodAnnotations($method); + if ($method->getDeclaringClass()->name !== $class->name) { + continue; + } - if ($annotations) { - foreach ($annotations as $key => $annot) { - if ( ! is_numeric($key)) { - continue; - } - $annotations[get_class($annot)] = $annot; - } - } - - if (isset($annotations['Doctrine\ORM\Mapping\PrePersist'])) { - $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::prePersist); - } - - if (isset($annotations['Doctrine\ORM\Mapping\PostPersist'])) { - $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postPersist); - } - - if (isset($annotations['Doctrine\ORM\Mapping\PreUpdate'])) { - $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preUpdate); - } - - if (isset($annotations['Doctrine\ORM\Mapping\PostUpdate'])) { - $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postUpdate); - } - - if (isset($annotations['Doctrine\ORM\Mapping\PreRemove'])) { - $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preRemove); - } - - if (isset($annotations['Doctrine\ORM\Mapping\PostRemove'])) { - $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postRemove); - } - - if (isset($annotations['Doctrine\ORM\Mapping\PostLoad'])) { - $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postLoad); - } - - if (isset($annotations['Doctrine\ORM\Mapping\PreFlush'])) { - $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preFlush); - } + foreach ($this->getMethodCallbacks($method) as $value) { + list($callback, $event) = $value; + $metadata->addLifecycleCallback($callback, $event); } } } @@ -488,9 +474,55 @@ class AnnotationDriver extends AbstractAnnotationDriver } /** - * Parses the given JoinColumn as array. + * Parses the given method. * - * @param JoinColumn $joinColumn + * @param \ReflectionMethod $method + * @return array + */ + private function getMethodCallbacks(\ReflectionMethod $method) + { + $callbacks = array(); + $annotations = $this->reader->getMethodAnnotations($method); + + foreach ($annotations as $annot) { + if ($annot instanceof \Doctrine\ORM\Mapping\PrePersist) { + $callbacks[] = array($method->name, Events::prePersist); + } + + if ($annot instanceof \Doctrine\ORM\Mapping\PostPersist) { + $callbacks[] = array($method->name, Events::postPersist); + } + + if ($annot instanceof \Doctrine\ORM\Mapping\PreUpdate) { + $callbacks[] = array($method->name, Events::preUpdate); + } + + if ($annot instanceof \Doctrine\ORM\Mapping\PostUpdate) { + $callbacks[] = array($method->name, Events::postUpdate); + } + + if ($annot instanceof \Doctrine\ORM\Mapping\PreRemove) { + $callbacks[] = array($method->name, Events::preRemove); + } + + if ($annot instanceof \Doctrine\ORM\Mapping\PostRemove) { + $callbacks[] = array($method->name, Events::postRemove); + } + + if ($annot instanceof \Doctrine\ORM\Mapping\PostLoad) { + $callbacks[] = array($method->name, Events::postLoad); + } + + if ($annot instanceof \Doctrine\ORM\Mapping\PreFlush) { + $callbacks[] = array($method->name, Events::preFlush); + } + } + + return $callbacks; + } + + /** + * Parse the given JoinColumn as array * * @return array */ diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index 04bf2dedc..dc577d5c0 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -64,3 +64,4 @@ require_once __DIR__.'/../AssociationOverride.php'; require_once __DIR__.'/../AssociationOverrides.php'; require_once __DIR__.'/../AttributeOverride.php'; require_once __DIR__.'/../AttributeOverrides.php'; +require_once __DIR__.'/../EntityListeners.php'; diff --git a/lib/Doctrine/ORM/Mapping/EntityListeners.php b/lib/Doctrine/ORM/Mapping/EntityListeners.php new file mode 100644 index 000000000..3d08b2dc3 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/EntityListeners.php @@ -0,0 +1,41 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +/** + * The EntityListeners annotation specifies the callback listener classes to be used for an entity or mapped superclass. + * The EntityListeners annotation may be applied to an entity class or mapped superclass. + * + * @author Fabio B. Silva + * @since 2.4 + * + * @Annotation + * @Target("CLASS") + */ +final class EntityListeners implements Annotation +{ + /** + * Specifies the names of the entity listeners. + * + * @var array + */ + public $value; +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Company/CompanyContract.php b/tests/Doctrine/Tests/Models/Company/CompanyContract.php index bc8503dfe..71aa6f060 100644 --- a/tests/Doctrine/Tests/Models/Company/CompanyContract.php +++ b/tests/Doctrine/Tests/Models/Company/CompanyContract.php @@ -7,6 +7,7 @@ namespace Doctrine\Tests\Models\Company; * @Table(name="company_contracts") * @InheritanceType("SINGLE_TABLE") * @DiscriminatorColumn(name="discr", type="string") + * @EntityListeners({"Doctrine\Tests\Models\Company\ContractSubscriber"}) * @DiscriminatorMap({ * "fix" = "CompanyFixContract", * "flexible" = "CompanyFlexContract", diff --git a/tests/Doctrine/Tests/Models/Company/ContractSubscriber.php b/tests/Doctrine/Tests/Models/Company/ContractSubscriber.php new file mode 100644 index 000000000..e6557c0fc --- /dev/null +++ b/tests/Doctrine/Tests/Models/Company/ContractSubscriber.php @@ -0,0 +1,31 @@ +_em->getClassMetadata('Doctrine\Tests\Models\Company\CompanyFixContract'); + $fixClass = $this->_em->getClassMetadata('Doctrine\Tests\Models\Company\CompanyFlexContract'); + + $this->assertNull(ContractSubscriber::$instances); + $this->assertNull(ContractSubscriber::$prePersistCalls); + $this->assertNull(ContractSubscriber::$postPersisCalls); + + $fix = new CompanyFixContract(); + $fixArg = new LifecycleEventArgs($fix, $this->_em); + + $flex = new CompanyFlexContract(); + $flexArg = new LifecycleEventArgs($fix, $this->_em); + + $fixClass->dispatchEntityListeners(Events::prePersist, $fix, $fixArg); + $flexClass->dispatchEntityListeners(Events::prePersist, $flex, $flexArg); + + $this->assertSame($fix, ContractSubscriber::$prePersistCalls[0][0]); + $this->assertSame($fixArg, ContractSubscriber::$prePersistCalls[0][1]); + + $this->assertCount(1, ContractSubscriber::$instances); + $this->assertNull(ContractSubscriber::$postPersisCalls); + } +} \ No newline at end of file