diff --git a/lib/Doctrine/ORM/Event/LifecycleEventArgs.php b/lib/Doctrine/ORM/Event/LifecycleEventArgs.php index 37f5d0645..612a31f2d 100644 --- a/lib/Doctrine/ORM/Event/LifecycleEventArgs.php +++ b/lib/Doctrine/ORM/Event/LifecycleEventArgs.php @@ -7,7 +7,7 @@ class LifecycleEventArgs extends \Doctrine\Common\EventArgs private $_em; private $_entity; - public function __construct($entity, \Doctrine\ORM\EntityManager $em) + public function __construct($entity) { $this->_entity = $entity; } @@ -16,9 +16,4 @@ class LifecycleEventArgs extends \Doctrine\Common\EventArgs { return $this->_entity; } - - public function getEntityManager() - { - return $this->_em; - } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Events.php b/lib/Doctrine/ORM/Events.php index 7d0ca462d..8667203a7 100644 --- a/lib/Doctrine/ORM/Events.php +++ b/lib/Doctrine/ORM/Events.php @@ -32,13 +32,76 @@ namespace Doctrine\ORM; final class Events { private function __construct() {} - + /** + * The preDelete event occurs for a given entity before the respective + * EntityManager delete operation for that entity is executed. + * + * This is an entity lifecycle event. + * + * @var string + */ const preDelete = 'preDelete'; + /** + * The postDelete event occurs for an entity after the entity has + * been deleted. It will be invoked after the database delete operations. + * + * This is an entity lifecycle event. + * + * @var string + */ const postDelete = 'postDelete'; - const preInsert = 'preInsert'; - const postInsert = 'postInsert'; + /** + * The preSave event occurs for a given entity before the respective + * EntityManager save operation for that entity is executed. + * + * This is an entity lifecycle event. + * + * @var string + */ + const preSave = 'preSave'; + /** + * The postSave event occurs for an entity after the entity has + * been made persistent. It will be invoked after the database insert operations. + * Generated primary key values are available in the postSave event. + * + * This is an entity lifecycle event. + * + * @var string + */ + const postSave = 'postSave'; + /** + * The preUpdate event occurs before the database update operations to + * entity data. + * + * This is an entity lifecycle event. + * + * @var string + */ const preUpdate = 'preUpdate'; + /** + * The postUpdate event occurs after the database update operations to + * entity data. + * + * This is an entity lifecycle event. + * + * @var string + */ const postUpdate = 'postUpdate'; + /** + * The postLoad event occurs for an entity after the entity has been loaded + * into the current EntityManager from the database or after the refresh operation + * has been applied to it. + * + * This is an entity lifecycle event. + * + * @var string + */ const postLoad = 'postLoad'; + /** + * The loadClassMetadata event occurs after the mapping metadata for a class + * has been loaded from a mapping source (annotations/xml/yaml). + * + * @var string + */ const loadClassMetadata = 'loadClassMetadata'; } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 474b237a5..8cd291aa8 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -231,9 +231,39 @@ class AnnotationDriver implements Driver $mapping['mappedBy'] = $manyToManyAnnot->mappedBy; $mapping['cascade'] = $manyToManyAnnot->cascade; $metadata->mapManyToMany($mapping); - } - + } } + + // Evaluate LifecycleListener annotation + if (($lifecycleListenerAnnot = $this->_reader->getClassAnnotation($class, 'Doctrine\ORM\Mapping\LifecycleListener'))) { + foreach ($class->getMethods() as $method) { + if ($method->isPublic()) { + $annotations = $this->_reader->getMethodAnnotations($method); + if (isset($annotations['Doctrine\ORM\Mapping\PreSave'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preSave); + } + if (isset($annotations['Doctrine\ORM\Mapping\PostSave'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postSave); + } + 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\PreDelete'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preDelete); + } + if (isset($annotations['Doctrine\ORM\Mapping\PostDelete'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postDelete); + } + if (isset($annotations['Doctrine\ORM\Mapping\PostLoad'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postLoad); + } + } + } + } + } /** diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index 8dd747631..117e6dc2f 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -103,6 +103,7 @@ final class SequenceGenerator extends \Doctrine\Common\Annotations\Annotation { final class ChangeTrackingPolicy extends \Doctrine\Common\Annotations\Annotation {} /* Annotations for lifecycle callbacks */ +final class LifecycleListener extends \Doctrine\Common\Annotations\Annotation {} final class PreSave extends \Doctrine\Common\Annotations\Annotation {} final class PostSave extends \Doctrine\Common\Annotations\Annotation {} final class PreUpdate extends \Doctrine\Common\Annotations\Annotation {} diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 63e2746ef..fdd51d593 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -25,6 +25,7 @@ use Doctrine\Common\Collections\Collection; use Doctrine\Common\DoctrineException; use Doctrine\Common\PropertyChangedListener; use Doctrine\ORM\Events; +use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\ORM\Internal\CommitOrderCalculator; use Doctrine\ORM\Internal\CommitOrderNode; use Doctrine\ORM\PersistentCollection; @@ -634,18 +635,28 @@ class UnitOfWork implements PropertyChangedListener { $className = $class->name; $persister = $this->getEntityPersister($className); + + $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postSave]); + $hasListeners = $this->_evm->hasListeners(Events::postSave); + if ($hasLifecycleCallbacks || $hasListeners) { + $entities = array(); + } + foreach ($this->_entityInsertions as $oid => $entity) { if (get_class($entity) == $className) { $persister->addInsert($entity); unset($this->_entityInsertions[$oid]); + if ($hasLifecycleCallbacks || $hasListeners) { + $entities[] = $entity; + } } } $postInsertIds = $persister->executeInserts(); if ($postInsertIds) { + // Persister returned a post-insert IDs foreach ($postInsertIds as $id => $entity) { - // Persister returned a post-insert ID $oid = spl_object_hash($entity); $idField = $class->identifier[0]; $class->reflFields[$idField]->setValue($entity, $id); @@ -655,6 +666,17 @@ class UnitOfWork implements PropertyChangedListener $this->addToIdentityMap($entity); } } + + if ($hasLifecycleCallbacks || $hasListeners) { + foreach ($entities as $entity) { + if ($hasLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::postSave, $entity); + } + if ($hasListeners) { + $this->_evm->dispatchEvent(Events::postSave, new LifecycleEventArgs($entity)); + } + } + } } /** @@ -666,14 +688,36 @@ class UnitOfWork implements PropertyChangedListener { $className = $class->name; $persister = $this->getEntityPersister($className); + + $hasPreUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::preUpdate]); + $hasPreUpdateListeners = $this->_evm->hasListeners(Events::preUpdate); + $hasPostUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postUpdate]); + $hasPostUpdateListeners = $this->_evm->hasListeners(Events::postUpdate); + foreach ($this->_entityUpdates as $oid => $entity) { if (get_class($entity) == $className) { - //TODO: Fire preUpdate + if ($hasPreUpdateLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::preUpdate, $entity); + if ( ! $hasPreUpdateListeners) { + // Need to recompute entity changeset to detect changes made in the callback. + $this->computeSingleEntityChangeSet($class, $entity); + } + } + if ($hasPreUpdateListeners) { + $this->_evm->dispatchEvent(Events::preUpdate, new LifecycleEventArgs($entity)); + // Need to recompute entity changeset to detect changes made in the listener. + $this->computeSingleEntityChangeSet($class, $entity); + } $persister->update($entity); unset($this->_entityUpdates[$oid]); - //TODO: Fire postUpdate + if ($hasPostUpdateLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::postUpdate, $entity); + } + if ($hasPostUpdateListeners) { + $this->_evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entity)); + } } } } @@ -687,12 +731,21 @@ class UnitOfWork implements PropertyChangedListener { $className = $class->name; $persister = $this->getEntityPersister($className); + + $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postDelete]); + $hasListeners = $this->_evm->hasListeners(Events::postDelete); + foreach ($this->_entityDeletions as $oid => $entity) { if (get_class($entity) == $className) { $persister->delete($entity); unset($this->_entityDeletions[$oid]); - //TODO: Fire postDelete + if ($hasLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::postDelete, $entity); + } + if ($hasListeners) { + $this->_evm->dispatchEvent(Events::postDelete, new LifecycleEventArgs($entity)); + } } } } @@ -1109,7 +1162,12 @@ class UnitOfWork implements PropertyChangedListener } break; case self::STATE_NEW: - //TODO: Fire preSave lifecycle event + if (isset($class->lifecycleCallbacks[Events::preSave])) { + $class->invokeLifecycleCallbacks(Events::preSave, $entity); + } + if ($this->_evm->hasListeners(Events::preSave)) { + $this->_evm->dispatchEvent(Events::preSave, new LifecycleEventArgs($entity)); + } $idGen = $class->idGenerator; if ($idGen->isPostInsertGenerator()) { @@ -1172,15 +1230,20 @@ class UnitOfWork implements PropertyChangedListener } $visited[$oid] = $entity; // mark visited - - //TODO: Fire preDelete + $class = $this->_em->getClassMetadata(get_class($entity)); switch ($this->getEntityState($entity)) { case self::STATE_NEW: case self::STATE_DELETED: // nothing to do break; case self::STATE_MANAGED: + if (isset($class->lifecycleCallbacks[Events::preDelete])) { + $class->invokeLifecycleCallbacks(Events::preDelete, $entity); + } + if ($this->_evm->hasListeners(Events::preDelete)) { + $this->_evm->dispatchEvent(Events::preDelete, new LifecycleEventArgs($entity)); + } $this->registerDeleted($entity); break; case self::STATE_DETACHED: @@ -1227,6 +1290,15 @@ class UnitOfWork implements PropertyChangedListener } else { $managedCopy = $this->_em->find($class->name, $id); } + + if ($class->isVersioned) { + $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy); + $entityVersion = $class->reflFields[$class->versionField]->getValue($entity); + // Throw exception if versions dont match. + if ($managedCopyVersion != $entity) { + throw OptimisticLockException::versionMismatch(); + } + } // Merge state of $entity into existing (managed) entity foreach ($class->reflFields as $name => $prop) { @@ -1463,13 +1535,13 @@ class UnitOfWork implements PropertyChangedListener } } - /*if (isset($class->lifecycleCallbacks[Events::postLoad])) { + if (isset($class->lifecycleCallbacks[Events::postLoad])) { $class->invokeLifecycleCallbacks(Events::postLoad, $entity); } if ($this->_evm->hasListeners(Events::postLoad)) { - $this->_evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity, $this->_em)); + $this->_evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity)); } - */ + return $entity; } diff --git a/tests/Doctrine/Tests/ORM/Functional/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/AllTests.php index 22c8c7364..304030f98 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Functional/AllTests.php @@ -35,6 +35,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToManySelfReferentialAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ManyToManySelfReferentialAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ReferenceProxyTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\LifecycleCallbackTest'); $suite->addTest(Locking\AllTests::suite()); diff --git a/tests/Doctrine/Tests/ORM/Functional/LifecycleCallbackTest.php b/tests/Doctrine/Tests/ORM/Functional/LifecycleCallbackTest.php new file mode 100644 index 000000000..604edf4b7 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/LifecycleCallbackTest.php @@ -0,0 +1,86 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\LifecycleCallbackTestEntity') + )); + } catch (\Exception $e) { + // Swallow all exceptions. We do not test the schema tool here. + } + } + + public function testPreSavePostSaveCallbacksAreInvoked() + { + $entity = new LifecycleCallbackTestEntity; + $entity->value = 'hello'; + $this->_em->save($entity); + $this->_em->flush(); + + $this->assertTrue($entity->preSaveCallbackInvoked); + $this->assertTrue($entity->postSaveCallbackInvoked); + + $this->_em->clear(); + + $query = $this->_em->createQuery("select e from Doctrine\Tests\ORM\Functional\LifecycleCallbackTestEntity e"); + $result = $query->getResultList(); + $this->assertTrue($result[0]->postLoadCallbackInvoked); + + $result[0]->value = 'hello again'; + + $this->_em->flush(); + + $this->assertEquals('changed from preUpdate callback!', $result[0]->value); + } +} + +/** + * @Entity + * @LifecycleListener + * @Table(name="lifecycle_callback_test_entity") + */ +class LifecycleCallbackTestEntity +{ + /* test stuff */ + public $preSaveCallbackInvoked = false; + public $postSaveCallbackInvoked = false; + public $postLoadCallbackInvoked = false; + + /** + * @Id @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + private $id; + /** + * @Column(type="string", length=255) + */ + public $value; + + /** @PreSave */ + public function doStuffOnPreSave() { + $this->preSaveCallbackInvoked = true; + } + + /** @PostSave */ + public function doStuffOnPostSave() { + $this->postSaveCallbackInvoked = true; + } + + /** @PostLoad */ + public function doStuffOnPostLoad() { + $this->postLoadCallbackInvoked = true; + } + + /** @PreUpdate */ + public function doStuffOnPreUpdate() { + $this->value = 'changed from preUpdate callback!'; + } +} +