Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Cache/Persister/Entity/AbstractEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ public function loadAll(
/**
* {@inheritDoc}
*/
public function loadById(array $identifier, object|null $entity = null): object|null
public function loadById(array $identifier, object|null $entity = null, bool $readOnly = false): object|null
{
$cacheKey = new EntityCacheKey($this->class->rootEntityName, $identifier);
$cacheEntry = $this->region->get($cacheKey);
Expand All @@ -391,7 +391,7 @@ public function loadById(array $identifier, object|null $entity = null): object|
}
}

$entity = $this->persister->loadById($identifier, $entity);
$entity = $this->persister->loadById($identifier, $entity, $readOnly);

if ($entity === null) {
return null;
Expand Down
4 changes: 2 additions & 2 deletions src/Decorator/EntityManagerDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|i
$this->wrapped->lock($entity, $lockMode, $lockVersion);
}

public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null
public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null, bool $readOnly = false): object|null
{
return $this->wrapped->find($className, $id, $lockMode, $lockVersion);
return $this->wrapped->find($className, $id, $lockMode, $lockVersion, $readOnly);
}

public function refresh(object $object, LockMode|int|null $lockMode = null): void
Expand Down
12 changes: 8 additions & 4 deletions src/EntityManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ public function flush(): void
/**
* {@inheritDoc}
*/
public function find($className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null
public function find($className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null, bool $readOnly = false): object|null
{
$class = $this->metadataFactory->getMetadataFor(ltrim($className, '\\'));

Expand Down Expand Up @@ -343,14 +343,18 @@ public function find($className, mixed $id, LockMode|int|null $lockMode = null,
break;
}

if (! $readOnly && $unitOfWork->isReadOnly($entity)) {
$unitOfWork->unmarkReadOnly($entity);
}

return $entity; // Hit!
}

$persister = $unitOfWork->getEntityPersister($class->name);

switch (true) {
case $lockMode === LockMode::OPTIMISTIC:
$entity = $persister->load($sortedId);
$entity = $persister->load($sortedId, hints: [Query::HINT_READ_ONLY => $readOnly]);

if ($entity !== null) {
$unitOfWork->lock($entity, $lockMode, $lockVersion);
Expand All @@ -360,10 +364,10 @@ public function find($className, mixed $id, LockMode|int|null $lockMode = null,

case $lockMode === LockMode::PESSIMISTIC_READ:
case $lockMode === LockMode::PESSIMISTIC_WRITE:
return $persister->load($sortedId, null, null, [], $lockMode);
return $persister->load($sortedId, null, null, [Query::HINT_READ_ONLY => $readOnly], $lockMode);

default:
return $persister->loadById($sortedId);
return $persister->loadById($sortedId, readOnly: $readOnly);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/EntityManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public function createQueryBuilder(): QueryBuilder;
*
* @template T of object
*/
public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null;
public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null, bool $readOnly = false): object|null;

/**
* Refreshes the persistent state of an object from the database,
Expand Down
4 changes: 2 additions & 2 deletions src/Persisters/Entity/BasicEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -748,9 +748,9 @@ public function load(
/**
* {@inheritDoc}
*/
public function loadById(array $identifier, object|null $entity = null): object|null
public function loadById(array $identifier, object|null $entity = null, bool $readOnly = false): object|null
{
return $this->load($identifier, $entity);
return $this->load($identifier, $entity, hints: [Query::HINT_READ_ONLY => $readOnly]);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Persisters/Entity/EntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public function load(
*
* @todo Check parameters
*/
public function loadById(array $identifier, object|null $entity = null): object|null;
public function loadById(array $identifier, object|null $entity = null, bool $readOnly = false): object|null;

/**
* Loads an entity of this persister's mapped class as part of a single-valued
Expand Down
18 changes: 15 additions & 3 deletions src/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -3080,9 +3080,6 @@ private static function objToStr(object $obj): string
/**
* Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
*
* This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
* on this object that might be necessary to perform a correct update.
*
* @throws ORMInvalidArgumentException
*/
public function markReadOnly(object $object): void
Expand All @@ -3094,6 +3091,21 @@ public function markReadOnly(object $object): void
$this->readOnlyObjects[spl_object_id($object)] = true;
}

/**
* Unmarks an entity as read-only so that it will be considered for updates during UnitOfWork#commit()
* even if it was previously fetched as read-only.
*
* @throws ORMInvalidArgumentException
*/
public function unmarkReadOnly(object $object): void
{
if (! $this->isInIdentityMap($object)) {
throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
}

unset($this->readOnlyObjects[spl_object_id($object)]);
}

/**
* Is this entity read only?
*
Expand Down
2 changes: 1 addition & 1 deletion tests/Performance/Mock/NonLoadingPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public function __construct(
$this->class = $class;
}

public function loadById(array $identifier, object|null $entity = null): object|null
public function loadById(array $identifier, object|null $entity = null, bool $readOnly = false): object|null
{
return $entity ?? new ($this->class->name)();
}
Expand Down
4 changes: 2 additions & 2 deletions tests/Performance/Mock/NonProxyLoadingEntityManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@ public function hasFilters(): bool
return $this->realEntityManager->hasFilters();
}

public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null
public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null, bool $readOnly = false): object|null
{
return $this->realEntityManager->find($className, $id, $lockMode, $lockVersion);
return $this->realEntityManager->find($className, $id, $lockMode, $lockVersion, $readOnly);
}

public function persist(object $object): void
Expand Down
61 changes: 35 additions & 26 deletions tests/Tests/ORM/Functional/ReadOnlyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function testReadOnlyEntityNeverChangeTracked(): void
$this->_em->flush();
$this->_em->clear();

$dbReadOnly = $this->_em->find(ReadOnlyEntity::class, $readOnly->id);
$dbReadOnly = $this->getWithoutReadOnlyHint($readOnly->id);
self::assertEquals('Test1', $dbReadOnly->name);
self::assertEquals(1234, $dbReadOnly->numericValue);
}
Expand Down Expand Up @@ -77,13 +77,7 @@ public function testReadOnlyQueryHint(): void
$this->_em->flush();
$this->_em->clear();

$dql = 'SELECT u FROM ' . ReadOnlyEntity::class . ' u WHERE u.id = ?1';

$query = $this->_em->createQuery($dql);
$query->setParameter(1, $user->id);
$query->setHint(Query::HINT_READ_ONLY, true);

$user = $query->getSingleResult();
$user = $this->getWithReadOnlyHint($user->id, readOnly: true);

self::assertTrue($this->_em->getUnitOfWork()->isReadOnly($user));
}
Expand All @@ -97,11 +91,7 @@ public function testNotReadOnlyQueryHint(): void
$this->_em->flush();
$this->_em->clear();

$query = $this->_em->createQuery('SELECT u FROM ' . ReadOnlyEntity::class . ' u WHERE u.id = ?1');
$query->setParameter(1, $user->id);
$query->setHint(Query::HINT_READ_ONLY, false);

$user = $query->getSingleResult();
$user = $this->getWithReadOnlyHint($user->id, readOnly: false);

self::assertFalse($this->_em->getUnitOfWork()->isReadOnly($user));
}
Expand All @@ -117,13 +107,7 @@ public function testNotReadOnlyIfObjectWasProxyBefore(): void

$user = $this->_em->getReference(ReadOnlyEntity::class, $user->id);

$dql = 'SELECT u FROM ' . ReadOnlyEntity::class . ' u WHERE u.id = ?1';

$query = $this->_em->createQuery($dql);
$query->setParameter(1, $user->id);
$query->setHint(Query::HINT_READ_ONLY, true);

$user = $query->getSingleResult();
$user = $this->getWithReadOnlyHint($user->id, readOnly: true);

self::assertFalse($this->_em->getUnitOfWork()->isReadOnly($user));
}
Expand All @@ -137,18 +121,43 @@ public function testNotReadOnlyIfObjectWasKnownBefore(): void
$this->_em->flush();
$this->_em->clear();

$userIntoIdentityMap = $this->_em->find(ReadOnlyEntity::class, $user->id);
$userIntoIdentityMap = $this->getWithoutReadOnlyHint($user->id);

$dql = 'SELECT u FROM ' . ReadOnlyEntity::class . ' u WHERE u.id = ?1';
$user = $this->getWithReadOnlyHint($user->id, readOnly: true);

$query = $this->_em->createQuery($dql);
$query->setParameter(1, $user->id);
$query->setHint(Query::HINT_READ_ONLY, true);
self::assertFalse($this->_em->getUnitOfWork()->isReadOnly($user));
}

public function testWriteableEvenIfObjectWasKnownAsReadOnlyBefore(): void
{
$user = new ReadOnlyEntity('Théo', 1234);

$user = $query->getSingleResult();
$this->_em->persist($user);

$this->_em->flush();
$this->_em->clear();

$userIntoIdentityMap = $this->getWithReadOnlyHint($user->id, readOnly: true);
$user = $this->getWithoutReadOnlyHint($user->id);

self::assertFalse($this->_em->getUnitOfWork()->isReadOnly($user));
}

private function getWithoutReadOnlyHint(int $id): ReadOnlyEntity
{
return $this->_em->find(ReadOnlyEntity::class, $id);
}

private function getWithReadOnlyHint(int $id, bool $readOnly): ReadOnlyEntity
{
$dql = 'SELECT u FROM ' . ReadOnlyEntity::class . ' u WHERE u.id = ?1';

$query = $this->_em->createQuery($dql);
$query->setParameter(1, $id);
$query->setHint(Query::HINT_READ_ONLY, $readOnly);

return $query->getSingleResult();
}
}

#[Entity(readOnly: true)]
Expand Down