mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 06:52:09 +01:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17500f56ea | ||
|
|
fc2f724e2d | ||
|
|
2f9e98754b | ||
|
|
bb5524099c | ||
|
|
3a8cafe228 | ||
|
|
8259a16681 | ||
|
|
5577d51c44 | ||
|
|
d1922a3065 |
@@ -13,7 +13,7 @@ for all our domain objects.
|
||||
.. note::
|
||||
|
||||
The notify change tracking policy is deprecated and will be removed in ORM 3.0.
|
||||
(`Details <https://github.com/doctrine/orm/issues/8383>`_)
|
||||
(\ `Details <https://github.com/doctrine/orm/issues/8383>`_)
|
||||
|
||||
Implementing NotifyPropertyChanged
|
||||
----------------------------------
|
||||
|
||||
@@ -58,6 +58,10 @@ First Attributes:
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
|
||||
use Doctrine\ORM\Mapping\PrePersist;
|
||||
use Doctrine\ORM\Mapping\PreUpdate;
|
||||
|
||||
#[Entity]
|
||||
#[HasLifecycleCallbacks]
|
||||
|
||||
@@ -47,8 +47,7 @@ mapping metadata:
|
||||
- :doc:`Attributes <attributes-reference>`
|
||||
- :doc:`XML <xml-mapping>`
|
||||
- :doc:`PHP code <php-mapping>`
|
||||
- :doc:`Docblock Annotations <annotations-reference>` (deprecated and
|
||||
will be removed in ``doctrine/orm`` 3.0)
|
||||
- :doc:`Docblock Annotations <annotations-reference>` (deprecated and will be removed in ``doctrine/orm`` 3.0)
|
||||
- :doc:`YAML <yaml-mapping>` (deprecated and will be removed in ``doctrine/orm`` 3.0.)
|
||||
|
||||
This manual will usually show mapping metadata via attributes, though
|
||||
|
||||
@@ -63,7 +63,7 @@ Notify
|
||||
.. note::
|
||||
|
||||
The notify change tracking policy is deprecated and will be removed in ORM 3.0.
|
||||
(`Details <https://github.com/doctrine/orm/issues/8383>`_)
|
||||
(\ `Details <https://github.com/doctrine/orm/issues/8383>`_)
|
||||
|
||||
This policy is based on the assumption that the entities notify
|
||||
interested listeners of changes to their properties. For that
|
||||
|
||||
@@ -1425,7 +1425,7 @@ userland:
|
||||
reloading this data. Partially loaded objects have to be passed to
|
||||
``EntityManager::refresh()`` if they are to be reloaded fully from
|
||||
the database. This query hint is deprecated and will be removed
|
||||
in the future (`Details <https://github.com/doctrine/orm/issues/8471>`_)
|
||||
in the future (\ `Details <https://github.com/doctrine/orm/issues/8471>`_)
|
||||
- ``Query::HINT_REFRESH`` - This query is used internally by
|
||||
``EntityManager::refresh()`` and can be used in userland as well.
|
||||
If you specify this hint and a query returns the data for an entity
|
||||
|
||||
@@ -215,6 +215,10 @@ specific to a particular entity class's lifecycle.
|
||||
<?php
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Event\PrePersistEventArgs;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
|
||||
use Doctrine\ORM\Mapping\PrePersist;
|
||||
use Doctrine\ORM\Mapping\PreUpdate;
|
||||
|
||||
#[Entity]
|
||||
#[HasLifecycleCallbacks]
|
||||
@@ -705,13 +709,21 @@ not directly mapped by Doctrine.
|
||||
- The ``postUpdate`` event occurs after the database
|
||||
update operations to entity data. It is not called for a DQL
|
||||
``UPDATE`` statement.
|
||||
- The ``postPersist`` event occurs for an entity after
|
||||
the entity has been made persistent. It will be invoked after the
|
||||
database insert operation for that entity. A generated primary key value for
|
||||
the entity will be available in the postPersist event.
|
||||
- The ``postPersist`` event occurs for an entity after the entity has
|
||||
been made persistent. It will be invoked after all database insert
|
||||
operations for new entities have been performed. Generated primary
|
||||
key values will be available for all entities at the time this
|
||||
event is triggered.
|
||||
- The ``postRemove`` event occurs for an entity after the
|
||||
entity has been deleted. It will be invoked after the database
|
||||
delete operations. It is not called for a DQL ``DELETE`` statement.
|
||||
entity has been deleted. It will be invoked after all database
|
||||
delete operations for entity rows have been executed. This event is
|
||||
not called for a DQL ``DELETE`` statement.
|
||||
|
||||
.. note::
|
||||
|
||||
At the time ``postPersist`` is called, there may still be collection and/or
|
||||
"extra" updates pending. The database may not yet be completely in
|
||||
sync with the entity states in memory, not even for the new entities.
|
||||
|
||||
.. warning::
|
||||
|
||||
@@ -720,6 +732,19 @@ not directly mapped by Doctrine.
|
||||
cascade remove relations. In this case, you should load yourself the proxy in
|
||||
the associated ``pre*`` event.
|
||||
|
||||
.. warning::
|
||||
|
||||
Making changes to entities and calling ``EntityManager::flush()`` from within
|
||||
``post*`` event handlers is strongly discouraged, and might be deprecated and
|
||||
eventually prevented in the future.
|
||||
|
||||
The reason is that it causes re-entrance into ``UnitOfWork::commit()`` while a commit
|
||||
is currently being processed. The ``UnitOfWork`` was never designed to support this,
|
||||
and its behavior in this situation is not covered by any tests.
|
||||
|
||||
This may lead to entity or collection updates being missed, applied only in parts and
|
||||
changes being lost at the end of the commit phase.
|
||||
|
||||
.. _reference-events-post-load:
|
||||
|
||||
postLoad
|
||||
|
||||
@@ -167,7 +167,7 @@ have produced, this is probably fine.
|
||||
|
||||
However, to mention known limitations, it is currently not possible to use "class"
|
||||
level `annotations <https://github.com/doctrine/orm/pull/1517>`_ or
|
||||
`attributes <https://github.com/doctrine/orm/issues/8868>` on traits, and attempts to
|
||||
`attributes <https://github.com/doctrine/orm/issues/8868>`_ on traits, and attempts to
|
||||
improve parser support for traits as `here <https://github.com/doctrine/annotations/pull/102>`_
|
||||
or `there <https://github.com/doctrine/annotations/pull/63>`_ have been abandoned
|
||||
due to complexity.
|
||||
|
||||
@@ -6,7 +6,7 @@ Partial Objects
|
||||
|
||||
Creating Partial Objects through DQL is deprecated and
|
||||
will be removed in the future, use data transfer object
|
||||
support in DQL instead. (`Details
|
||||
support in DQL instead. (\ `Details
|
||||
<https://github.com/doctrine/orm/issues/8471>`_)
|
||||
|
||||
A partial object is an object whose state is not fully initialized
|
||||
|
||||
@@ -137,7 +137,7 @@ optimize the performance of the Flush Operation:
|
||||
.. note::
|
||||
|
||||
Flush only a single entity with ``$entityManager->flush($entity)`` is deprecated and will be removed in ORM 3.0.
|
||||
(`Details <https://github.com/doctrine/orm/issues/8459>`_)
|
||||
(\ `Details <https://github.com/doctrine/orm/issues/8459>`_)
|
||||
|
||||
Query Internals
|
||||
---------------
|
||||
|
||||
@@ -18,7 +18,7 @@ before. There are some prerequisites for the tutorial that have to be
|
||||
installed:
|
||||
|
||||
- PHP (latest stable version)
|
||||
- Composer Package Manager (`Install Composer
|
||||
- Composer Package Manager (\ `Install Composer
|
||||
<https://getcomposer.org/doc/00-intro.md>`_)
|
||||
|
||||
The code of this tutorial is `available on Github <https://github.com/doctrine/doctrine2-orm-tutorial>`_.
|
||||
@@ -321,7 +321,7 @@ data in your storage, and later in your application when the data is loaded agai
|
||||
.. note::
|
||||
|
||||
This method, although very common, is inappropriate for Domain Driven
|
||||
Design (`DDD <https://en.wikipedia.org/wiki/Domain-driven_design>`_)
|
||||
Design (\ `DDD <https://en.wikipedia.org/wiki/Domain-driven_design>`_)
|
||||
where methods should represent real business operations and not simple
|
||||
property change, And business invariants should be maintained both in the
|
||||
application state (entities in this case) and in the database, with no
|
||||
|
||||
@@ -1012,6 +1012,7 @@ abstract class AbstractQuery
|
||||
*
|
||||
* @return bool|float|int|string|null The scalar result.
|
||||
*
|
||||
* @throws NoResultException If the query returned no result.
|
||||
* @throws NonUniqueResultException If the query result is not unique.
|
||||
*/
|
||||
public function getSingleScalarResult()
|
||||
|
||||
@@ -1164,13 +1164,13 @@ class UnitOfWork implements PropertyChangedListener
|
||||
*/
|
||||
private function executeInserts(): void
|
||||
{
|
||||
$entities = $this->computeInsertExecutionOrder();
|
||||
$entities = $this->computeInsertExecutionOrder();
|
||||
$eventsToDispatch = [];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$oid = spl_object_id($entity);
|
||||
$class = $this->em->getClassMetadata(get_class($entity));
|
||||
$persister = $this->getEntityPersister($class->name);
|
||||
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
|
||||
|
||||
$persister->addInsert($entity);
|
||||
|
||||
@@ -1197,10 +1197,24 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
|
||||
}
|
||||
|
||||
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
|
||||
|
||||
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
|
||||
$this->listenersInvoker->invoke($class, Events::postPersist, $entity, new PostPersistEventArgs($entity, $this->em), $invoke);
|
||||
$eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
|
||||
}
|
||||
}
|
||||
|
||||
// Defer dispatching `postPersist` events to until all entities have been inserted and post-insert
|
||||
// IDs have been assigned.
|
||||
foreach ($eventsToDispatch as $event) {
|
||||
$this->listenersInvoker->invoke(
|
||||
$event['class'],
|
||||
Events::postPersist,
|
||||
$event['entity'],
|
||||
new PostPersistEventArgs($event['entity'], $this->em),
|
||||
$event['invoke']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1270,7 +1284,8 @@ class UnitOfWork implements PropertyChangedListener
|
||||
*/
|
||||
private function executeDeletions(): void
|
||||
{
|
||||
$entities = $this->computeDeleteExecutionOrder();
|
||||
$entities = $this->computeDeleteExecutionOrder();
|
||||
$eventsToDispatch = [];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$oid = spl_object_id($entity);
|
||||
@@ -1295,9 +1310,20 @@ class UnitOfWork implements PropertyChangedListener
|
||||
}
|
||||
|
||||
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
|
||||
$this->listenersInvoker->invoke($class, Events::postRemove, $entity, new PostRemoveEventArgs($entity, $this->em), $invoke);
|
||||
$eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
|
||||
}
|
||||
}
|
||||
|
||||
// Defer dispatching `postRemove` events to until all entities have been removed.
|
||||
foreach ($eventsToDispatch as $event) {
|
||||
$this->listenersInvoker->invoke(
|
||||
$event['class'],
|
||||
Events::postRemove,
|
||||
$event['entity'],
|
||||
new PostRemoveEventArgs($event['entity'], $this->em),
|
||||
$event['invoke']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return list<object> */
|
||||
|
||||
@@ -4,12 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\ORM\Event\PostPersistEventArgs;
|
||||
use Doctrine\ORM\Event\PostRemoveEventArgs;
|
||||
use Doctrine\ORM\Event\PreFlushEventArgs;
|
||||
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\Persistence\Event\LifecycleEventArgs;
|
||||
use Doctrine\Tests\Models\Company\CompanyContractListener;
|
||||
use Doctrine\Tests\Models\Company\CompanyFixContract;
|
||||
use Doctrine\Tests\Models\Company\CompanyPerson;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
/** @group DDC-1955 */
|
||||
class EntityListenersTest extends OrmFunctionalTestCase
|
||||
@@ -96,6 +102,45 @@ class EntityListenersTest extends OrmFunctionalTestCase
|
||||
self::assertInstanceOf(LifecycleEventArgs::class, $this->listener->postPersistCalls[0][1]);
|
||||
}
|
||||
|
||||
public function testPostPersistCalledAfterAllInsertsHaveBeenPerformedAndIdsHaveBeenAssigned(): void
|
||||
{
|
||||
$object1 = new CompanyFixContract();
|
||||
$object1->setFixPrice(2000);
|
||||
|
||||
$object2 = new CompanyPerson();
|
||||
$object2->setName('J. Doe');
|
||||
|
||||
$this->_em->persist($object1);
|
||||
$this->_em->persist($object2);
|
||||
|
||||
$listener = new class ([$object1, $object2]) {
|
||||
/** @var array<object> */
|
||||
private $trackedObjects;
|
||||
|
||||
/** @var int */
|
||||
public $invocationCount = 0;
|
||||
|
||||
public function __construct(array $trackedObjects)
|
||||
{
|
||||
$this->trackedObjects = $trackedObjects;
|
||||
}
|
||||
|
||||
public function postPersist(PostPersistEventArgs $args): void
|
||||
{
|
||||
foreach ($this->trackedObjects as $object) {
|
||||
Assert::assertNotNull($object->getId());
|
||||
}
|
||||
|
||||
++$this->invocationCount;
|
||||
}
|
||||
};
|
||||
|
||||
$this->_em->getEventManager()->addEventListener(Events::postPersist, $listener);
|
||||
$this->_em->flush();
|
||||
|
||||
self::assertSame(2, $listener->invocationCount);
|
||||
}
|
||||
|
||||
public function testPreUpdateListeners(): void
|
||||
{
|
||||
$fix = new CompanyFixContract();
|
||||
@@ -175,4 +220,50 @@ class EntityListenersTest extends OrmFunctionalTestCase
|
||||
self::assertInstanceOf(CompanyFixContract::class, $this->listener->postRemoveCalls[0][0]);
|
||||
self::assertInstanceOf(LifecycleEventArgs::class, $this->listener->postRemoveCalls[0][1]);
|
||||
}
|
||||
|
||||
public function testPostRemoveCalledAfterAllRemovalsHaveBeenPerformed(): void
|
||||
{
|
||||
$object1 = new CompanyFixContract();
|
||||
$object1->setFixPrice(2000);
|
||||
|
||||
$object2 = new CompanyPerson();
|
||||
$object2->setName('J. Doe');
|
||||
|
||||
$this->_em->persist($object1);
|
||||
$this->_em->persist($object2);
|
||||
$this->_em->flush();
|
||||
|
||||
$listener = new class ($this->_em->getUnitOfWork(), [$object1, $object2]) {
|
||||
/** @var UnitOfWork */
|
||||
private $uow;
|
||||
|
||||
/** @var array<object> */
|
||||
private $trackedObjects;
|
||||
|
||||
/** @var int */
|
||||
public $invocationCount = 0;
|
||||
|
||||
public function __construct(UnitOfWork $uow, array $trackedObjects)
|
||||
{
|
||||
$this->uow = $uow;
|
||||
$this->trackedObjects = $trackedObjects;
|
||||
}
|
||||
|
||||
public function postRemove(PostRemoveEventArgs $args): void
|
||||
{
|
||||
foreach ($this->trackedObjects as $object) {
|
||||
Assert::assertFalse($this->uow->isInIdentityMap($object));
|
||||
}
|
||||
|
||||
++$this->invocationCount;
|
||||
}
|
||||
};
|
||||
|
||||
$this->_em->getEventManager()->addEventListener(Events::postRemove, $listener);
|
||||
$this->_em->remove($object1);
|
||||
$this->_em->remove($object2);
|
||||
$this->_em->flush();
|
||||
|
||||
self::assertSame(2, $listener->invocationCount);
|
||||
}
|
||||
}
|
||||
|
||||
76
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10869Test.php
Normal file
76
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10869Test.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\ORM\Event\PostPersistEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH10869Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH10869Entity::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testPostPersistListenerUpdatingObjectFieldWhileOtherInsertPending(): void
|
||||
{
|
||||
$entity1 = new GH10869Entity();
|
||||
$this->_em->persist($entity1);
|
||||
|
||||
$entity2 = new GH10869Entity();
|
||||
$this->_em->persist($entity2);
|
||||
|
||||
$this->_em->getEventManager()->addEventListener(Events::postPersist, new class {
|
||||
public function postPersist(PostPersistEventArgs $args): void
|
||||
{
|
||||
$object = $args->getObject();
|
||||
|
||||
$objectManager = $args->getObjectManager();
|
||||
$object->field = 'test ' . $object->id;
|
||||
$objectManager->flush();
|
||||
}
|
||||
});
|
||||
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
self::assertSame('test ' . $entity1->id, $entity1->field);
|
||||
self::assertSame('test ' . $entity2->id, $entity2->field);
|
||||
|
||||
$entity1Reloaded = $this->_em->find(GH10869Entity::class, $entity1->id);
|
||||
self::assertSame($entity1->field, $entity1Reloaded->field);
|
||||
|
||||
$entity2Reloaded = $this->_em->find(GH10869Entity::class, $entity2->id);
|
||||
self::assertSame($entity2->field, $entity2Reloaded->field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
*/
|
||||
class GH10869Entity
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*
|
||||
* @var ?int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", nullable=true)
|
||||
*
|
||||
* @var ?string
|
||||
*/
|
||||
public $field;
|
||||
}
|
||||
Reference in New Issue
Block a user