Skip to content

Commit

Permalink
Revert "Remove ability to clear the UoW partially (doctrine#9471)"
Browse files Browse the repository at this point in the history
This reverts commit 1c67f42.
  • Loading branch information
greg0ire committed Jul 9, 2023
1 parent 728703e commit 98134ca
Show file tree
Hide file tree
Showing 15 changed files with 274 additions and 42 deletions.
11 changes: 0 additions & 11 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,17 +393,6 @@ bugs/scenarios.
The method `UnitOfWork::merge()` has been removed. The method
`EntityManager::merge()` will throw an exception on each call.

## BC BREAK: Removed ability to partially clear entity manager and unit of work

* Passing an argument other than `null` to `EntityManager::clear()` will raise
an exception.
* The unit of work cannot be cleared partially anymore. Passing an argument to
`UnitOfWork::clear()` does not have any effect anymore; the unit of work is
cleared completely.
* The method `EntityRepository::clear()` has been removed.
* The methods `getEntityClass()` and `clearsAllEntities()` have been removed
from `OnClearEventArgs`.

## BC BREAK: Remove support for Doctrine Cache

The Doctrine Cache library is not supported anymore. The following methods
Expand Down
20 changes: 18 additions & 2 deletions lib/Doctrine/ORM/EntityManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Repository\RepositoryFactory;
use Doctrine\Persistence\Mapping\MappingException;
use Throwable;

use function array_keys;
use function is_array;
use function is_object;
use function is_string;
use function ltrim;
use function method_exists;

Expand Down Expand Up @@ -438,10 +440,24 @@ public function getPartialReference(string $entityName, mixed $identifier): obje
/**
* Clears the EntityManager. All entities that are currently managed
* by this EntityManager become detached.
*
* @param string|null $entityName if given, only entities of this type will get detached
*
* @throws ORMInvalidArgumentException If a non-null non-string value is given.
* @throws MappingException If a $entityName is given, but that entity is not
* found in the mappings.
*/
public function clear(): void
public function clear($entityName = null): void
{
$this->unitOfWork->clear();
if ($entityName !== null && ! is_string($entityName)) {
throw ORMInvalidArgumentException::invalidEntityName($entityName);
}

$this->unitOfWork->clear(
$entityName === null
? null
: $this->metadataFactory->getMetadataFor($entityName)->getName(),
);
}

public function close(): void
Expand Down
8 changes: 8 additions & 0 deletions lib/Doctrine/ORM/EntityRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ public function createResultSetMappingBuilder(string $alias): ResultSetMappingBu
return $rsm;
}

/**
* Clears the repository, causing all managed entities to become detached.
*/
public function clear(): void
{
$this->em->clear($this->class->rootEntityName);
}

/**
* Finds an entity by its primary key / identifier.
*
Expand Down
25 changes: 25 additions & 0 deletions lib/Doctrine/ORM/Event/OnClearEventArgs.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,29 @@
*/
class OnClearEventArgs extends BaseOnClearEventArgs
{
/** @param string|null $entityClass Optional entity class. */
public function __construct(EntityManagerInterface $em, private $entityClass = null)
{
parent::__construct($em);
}

/**
* Name of the entity class that is cleared, or empty if all are cleared.
*
* @return string|null
*/
public function getEntityClass()
{
return $this->entityClass;
}

/**
* Checks if event clears all entities.
*
* @return bool
*/
public function clearsAllEntities()
{
return $this->entityClass === null;
}
}
12 changes: 12 additions & 0 deletions lib/Doctrine/ORM/ORMInvalidArgumentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@ public static function invalidAssociation(ClassMetadata $targetClass, Associatio
));
}

/**
* Used when a given entityName hasn't the good type
*
* @param mixed $entityName The given entity (which shouldn't be a string)
*
* @return self
*/
public static function invalidEntityName($entityName)
{
return new self(sprintf('Entity name must be a string, %s given', get_debug_type($entityName)));
}

/**
* Helper method to show an object as string.
*/
Expand Down
76 changes: 53 additions & 23 deletions lib/Doctrine/ORM/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Cache\Persister\CachedPersister;
use Doctrine\ORM\Event\ListenersInvoker;
use Doctrine\ORM\Event\OnClearEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\PostPersistEventArgs;
Expand Down Expand Up @@ -2229,30 +2228,38 @@ public function getCommitOrderCalculator(): CommitOrderCalculator

/**
* Clears the UnitOfWork.
*/
public function clear(): void
{
$this->identityMap =
$this->entityIdentifiers =
$this->originalEntityData =
$this->entityChangeSets =
$this->entityStates =
$this->scheduledForSynchronization =
$this->entityInsertions =
$this->entityUpdates =
$this->entityDeletions =
$this->nonCascadedNewDetectedEntities =
$this->collectionDeletions =
$this->collectionUpdates =
$this->extraUpdates =
$this->readOnlyObjects =
$this->pendingCollectionElementRemovals =
$this->visitedCollections =
$this->eagerLoadingEntities =
$this->orphanRemovals = [];
*
* @param string|null $entityName if given, only entities of this type will get detached.
*
* @throws ORMInvalidArgumentException if an invalid entity name is given.
*/
public function clear(string|null $entityName = null): void
{
if ($entityName === null) {
$this->identityMap =
$this->entityIdentifiers =
$this->originalEntityData =
$this->entityChangeSets =
$this->entityStates =
$this->scheduledForSynchronization =
$this->entityInsertions =
$this->entityUpdates =
$this->entityDeletions =
$this->nonCascadedNewDetectedEntities =
$this->collectionDeletions =
$this->collectionUpdates =
$this->extraUpdates =
$this->readOnlyObjects =
$this->visitedCollections =
$this->eagerLoadingEntities =
$this->orphanRemovals = [];
} else {
$this->clearIdentityMapForEntityName($entityName);
$this->clearEntityInsertionsForEntityName($entityName);
}

if ($this->evm->hasListeners(Events::onClear)) {
$this->evm->dispatchEvent(Events::onClear, new OnClearEventArgs($this->em));
$this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em, $entityName));
}
}

Expand Down Expand Up @@ -3074,6 +3081,29 @@ public function hydrationComplete(): void
$this->hydrationCompleteHandler->hydrationComplete();
}

private function clearIdentityMapForEntityName(string $entityName): void
{
if (! isset($this->identityMap[$entityName])) {
return;
}

$visited = [];

foreach ($this->identityMap[$entityName] as $entity) {
$this->doDetach($entity, $visited, false);
}
}

private function clearEntityInsertionsForEntityName(string $entityName): void
{
foreach ($this->entityInsertions as $hash => $entity) {
// note: performance optimization - `instanceof` is much faster than a function call
if ($entity instanceof $entityName && get_class($entity) === $entityName) {
unset($this->entityInsertions[$hash]);
}
}
}

/** @throws MappingException if the entity has more than a single identifier. */
private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, mixed $identifierValue): mixed
{
Expand Down
10 changes: 10 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ parameters:
count: 1
path: lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php

-
message: "#^Result of && is always false\\.$#"
count: 1
path: lib/Doctrine/ORM/EntityManager.php

-
message: "#^Return type \\(Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadataFactory\\) of method Doctrine\\\\ORM\\\\EntityManager\\:\\:getMetadataFactory\\(\\) should be compatible with return type \\(Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadataFactory\\<Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadata\\<object\\>\\>\\) of method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getMetadataFactory\\(\\)$#"
count: 1
Expand All @@ -130,6 +135,11 @@ parameters:
count: 1
path: lib/Doctrine/ORM/EntityRepository.php

-
message: "#^Method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:clear\\(\\) invoked with 1 parameter, 0 required\\.$#"
count: 1
path: lib/Doctrine/ORM/EntityRepository.php

-
message: "#^Method Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\:\\:fullyQualifiedClassName\\(\\) should return class\\-string\\|null but returns string\\|null\\.$#"
count: 1
Expand Down
4 changes: 4 additions & 0 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,11 @@
<file src="lib/Doctrine/ORM/EntityManager.php">
<ArgumentTypeCoercion>
<code>$className</code>
<code>$entityName</code>
</ArgumentTypeCoercion>
<DocblockTypeContradiction>
<code><![CDATA[$entityName !== null && ! is_string($entityName)]]></code>
</DocblockTypeContradiction>
<InvalidReturnStatement>
<code>$entity</code>
<code>$entity</code>
Expand Down
55 changes: 55 additions & 0 deletions tests/Doctrine/Tests/ORM/EntityManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,27 @@
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Exception\EntityManagerClosed;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Persistence\Mapping\MappingException;
use Doctrine\Tests\Mocks\EntityManagerMock;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\Models\GeoNames\Country;
use Doctrine\Tests\OrmTestCase;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use stdClass;
use TypeError;

use function get_class;
use function random_int;
use function uniqid;

class EntityManagerTest extends OrmTestCase
{
private EntityManagerMock $entityManager;
Expand Down Expand Up @@ -180,4 +187,52 @@ public function testWrapInTransactionReThrowsThrowables(): void
self::assertFalse($this->entityManager->isOpen());
}
}

#[Group('6017')]
public function testClearManagerWithObject(): void
{
$entity = new Country('456', 'United Kingdom');

$this->expectException(ORMInvalidArgumentException::class);

$this->entityManager->clear($entity);
}

#[Group('6017')]
public function testClearManagerWithUnknownEntityName(): void
{
$this->expectException(MappingException::class);

$this->entityManager->clear(uniqid('nonExisting', true));
}

#[Group('6017')]
public function testClearManagerWithProxyClassName(): void
{
$proxy = $this->entityManager->getReference(Country::class, ['id' => random_int(457, 100000)]);

$entity = new Country('456', 'United Kingdom');

$this->entityManager->persist($entity);

self::assertTrue($this->entityManager->contains($entity));

$this->entityManager->clear(get_class($proxy));

self::assertFalse($this->entityManager->contains($entity));
}

#[Group('6017')]
public function testClearManagerWithNullValue(): void
{
$entity = new Country('456', 'United Kingdom');

$this->entityManager->persist($entity);

self::assertTrue($this->entityManager->contains($entity));

$this->entityManager->clear(null);

self::assertFalse($this->entityManager->contains($entity));
}
}
Loading

0 comments on commit 98134ca

Please sign in to comment.