Skip to content

Commit

Permalink
delete: use orignal entity data to generate collection rotues; update…
Browse files Browse the repository at this point in the history
…: also purge old and new collection routes
  • Loading branch information
usu committed May 18, 2024
1 parent 51788fd commit 3f604db
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 26 deletions.
76 changes: 64 additions & 12 deletions api/src/HttpCache/PurgeHttpCacheListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ public function __construct(private readonly IriConverterInterface|LegacyIriConv
* Collects tags from the previous and the current version of the updated entities to purge related documents.
*/
public function preUpdate(PreUpdateEventArgs $eventArgs): void {
$object = $eventArgs->getObject();
$this->addTagForItem($object);

$changeSet = $eventArgs->getEntityChangeSet();
$objectManager = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager();
$associationMappings = $objectManager->getClassMetadata(ClassUtils::getClass($eventArgs->getObject()))->getAssociationMappings();
Expand All @@ -73,7 +70,7 @@ public function preUpdate(PreUpdateEventArgs $eventArgs): void {
}

/**
* Collects tags from inserted and deleted entities, including relations.
* Collects tags from inserted, updated and deleted entities, including relations.
*/
public function onFlush(OnFlushEventArgs $eventArgs): void {
$em = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager();
Expand All @@ -84,10 +81,17 @@ public function onFlush(OnFlushEventArgs $eventArgs): void {
$this->gatherRelationTags($em, $entity);
}

foreach ($uow->getScheduledEntityDeletions() as $entity) {
foreach ($uow->getScheduledEntityUpdates() as $entity) {
$originalEntity = $this->getOriginalEntity($entity, $em);
$this->addTagForItem($entity);
$this->gatherResourceTags($entity);
$this->gatherRelationTags($em, $entity);
$this->gatherResourceTags($entity, $originalEntity);
}

foreach ($uow->getScheduledEntityDeletions() as $entity) {
$originalEntity = $this->getOriginalEntity($entity, $em);
$this->addTagForItem($originalEntity);
$this->gatherResourceTags($originalEntity);
$this->gatherRelationTags($em, $originalEntity);
}
}

Expand All @@ -98,7 +102,30 @@ public function postFlush(): void {
$this->cacheManager->flush();
}

private function gatherResourceTags(object $entity): void {
/**
* Computes the original state of the entity based on the current entity and on the changeset.
*/
private function getOriginalEntity($entity, $em) {
$uow = $em->getUnitOfWork();
$changeSet = $uow->getEntityChangeSet($entity);
$classMetadata = $em->getClassMetadata(ClassUtils::getClass($entity));

$originalEntity = clone $entity;
$em->detach($originalEntity);
foreach ($changeSet as $key => $value) {
$classMetadata->setFieldValue($originalEntity, $key, $value[0]);
}

return $originalEntity;
}

/**
* Purges all collections (GetCollection operations), in which entity is listed on top level.
*
* If oldEntity is provided, purge is only done if the IRI of the collection has changed
* (e.g. for updating period on a ScheduleEntry and the IRI changes from /periods/1/schedule_entries to /periods/2/schedule_entries)
*/
private function gatherResourceTags(object $entity, ?object $oldEntity = null): void {
try {
$resourceClass = $this->resourceClassResolver->getResourceClass($entity);
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
Expand All @@ -109,10 +136,7 @@ private function gatherResourceTags(object $entity): void {

foreach ($metadata->getOperations() ?? [] as $operation) {
if ($operation instanceof GetCollection) {
$iri = $this->iriConverter->getIriFromResource($entity, UrlGeneratorInterface::ABS_PATH, $operation);
if ($iri) {
$this->cacheManager->invalidateTags([$iri]);
}
$this->invalidateCollection($operation, $entity, $oldEntity);
}
}
$resourceIterator->next();
Expand All @@ -122,6 +146,34 @@ private function gatherResourceTags(object $entity): void {
}

/**
* Purges a single collection (GetCollection operation).
*
* If oldEntity is provided, purge is only done if the IRI of the collection has changed
* (e.g. for updating period on a ScheduleEntry and the IRI changes from /periods/1/schedule_entries to /periods/2/schedule_entries)
*/
private function invalidateCollection(GetCollection $operation, object $entity, ?object $oldEntity = null): void {
$iri = $this->iriConverter->getIriFromResource($entity, UrlGeneratorInterface::ABS_PATH, $operation);

if (!$iri) {
return;
}

if (!$oldEntity) {
$this->cacheManager->invalidateTags([$iri]);

return;
}

$oldIri = $this->iriConverter->getIriFromResource($oldEntity, UrlGeneratorInterface::ABS_PATH, $operation);
if ($iri !== $oldIri) {
$this->cacheManager->invalidateTags([$iri]);
$this->cacheManager->invalidateTags([$oldIri]);
}
}

/**
* Invalidate all relation tags of foreign objects ($relatedObject), in which $entity appears.
*
* @psalm-suppress UndefinedClass
*/
private function gatherRelationTags(EntityManagerInterface $em, object $entity): void {
Expand Down
83 changes: 69 additions & 14 deletions api/tests/HttpCache/PurgeHttpCacheListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,31 @@ protected function setUp(): void {
$this->uowProphecy = $this->prophesize(UnitOfWork::class);

$this->emProphecy = $this->prophesize(EntityManagerInterface::class);
$this->emProphecy->detach(Argument::any())->willReturn();
$this->emProphecy->getUnitOfWork()->willReturn($this->uowProphecy->reveal());
$dummyClassMetadata = new ClassMetadata(Dummy::class);
$dummyClassMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class, 'inversedBy' => 'dummies']);
$dummyClassMetadata->mapOneToOne(['fieldName' => 'relatedOwningDummy', 'targetEntity' => RelatedOwningDummy::class, 'inversedBy' => 'ownedDummy']);

$this->emProphecy->getClassMetadata(Dummy::class)->willReturn($dummyClassMetadata);
$classMetadataProphecy = $this->prophesize(ClassMetadata::class);
$classMetadataProphecy->getAssociationMappings()->willReturn([
'relatedDummy' => [
'targetEntity' => 'App\\Tests\\HttpCache\\Entity\\RelatedDummy',
'isOwningSide' => true,
'inversedBy' => 'dummies',
'mappedBy' => null,
],
'relatedOwningDummy' => [
'targetEntity' => 'App\\Tests\\HttpCache\\Entity\\RelatedOwningDummy',
'isOwningSide' => true,
'inversedBy' => 'ownedDummy',
'mappedBy' => null,
],
]);
$classMetadataProphecy->setFieldValue(Argument::any(), Argument::any(), Argument::any())->will(function ($args) {
$entity = $args[0];
$field = $args[1];
$value = $args[2];
$entity->{$field} = $value;
});
$this->emProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadataProphecy->reveal());

$this->propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
$this->propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedDummy')->willReturn(true);
Expand Down Expand Up @@ -148,12 +167,15 @@ public function testOnFlush(): void {
$resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class)->shouldBeCalled();
$resourceClassResolverProphecy->getResourceClass(Argument::type(DummyNoGetOperation::class))->willReturn(DummyNoGetOperation::class)->shouldBeCalled();

$uowProphecy = $this->prophesize(UnitOfWork::class);
$uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert1, $toInsert2])->shouldBeCalled();
$uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete1, $toDelete2, $toDeleteNoPurge])->shouldBeCalled();
$uowMock = $this->createMock(UnitOfWork::class);
$uowMock->method('getScheduledEntityInsertions')->willReturn([$toInsert1, $toInsert2]);
$uowMock->method('getScheduledEntityUpdates')->willReturn([]);
$uowMock->method('getScheduledEntityDeletions')->willReturn([$toDelete1, $toDelete2, $toDeleteNoPurge]);
$uowMock->method('getEntityChangeSet')->willReturn([]);

$emProphecy = $this->prophesize(EntityManagerInterface::class);
$emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled();
$emProphecy->getUnitOfWork()->willReturn($uowMock)->shouldBeCalled();
$emProphecy->detach(Argument::any())->willReturn();
$dummyClassMetadata = new ClassMetadata(Dummy::class);
$dummyClassMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class, 'inversedBy' => 'dummies']);
$dummyClassMetadata->mapOneToOne(['fieldName' => 'relatedOwningDummy', 'targetEntity' => RelatedOwningDummy::class, 'inversedBy' => 'ownedDummy']);
Expand Down Expand Up @@ -184,15 +206,13 @@ public function testPreUpdate(): void {
$dummy->setId('1');

$cacheManagerProphecy = $this->prophesize(CacheManager::class);
$cacheManagerProphecy->invalidateTags(['/dummies/1'])->shouldBeCalled()->willReturn($cacheManagerProphecy);
$cacheManagerProphecy->invalidateTags(['/related_dummies/old#dummies'])->shouldBeCalled()->willReturn($cacheManagerProphecy);
$cacheManagerProphecy->invalidateTags(['/related_dummies/new#dummies'])->shouldBeCalled()->willReturn($cacheManagerProphecy);
$cacheManagerProphecy->flush(Argument::any())->willReturn(0);

$metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);

$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
$iriConverterProphecy->getIriFromResource($dummy)->willReturn('/dummies/1')->shouldBeCalled();
$iriConverterProphecy->getIriFromResource($oldRelatedDummy)->willReturn('/related_dummies/old')->shouldBeCalled();
$iriConverterProphecy->getIriFromResource($newRelatedDummy)->willReturn('/related_dummies/new')->shouldBeCalled();

Expand Down Expand Up @@ -229,10 +249,8 @@ public function testNothingToPurge(): void {
$metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);

$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
$iriConverterProphecy->getIriFromResource($dummyNoGetOperation)->willReturn(null)->shouldBeCalled();

$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
$resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled();

$emProphecy = $this->prophesize(EntityManagerInterface::class);

Expand Down Expand Up @@ -274,6 +292,7 @@ public function testNotAResourceClass(): void {
$uowProphecy = $this->prophesize(UnitOfWork::class);
$uowProphecy->getScheduledEntityInsertions()->willReturn([$containNonResource])->shouldBeCalled();
$uowProphecy->getScheduledEntityDeletions()->willReturn([])->shouldBeCalled();
$uowProphecy->getScheduledEntityUpdates()->willReturn([])->shouldBeCalled();

$emProphecy = $this->prophesize(EntityManagerInterface::class);
$emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled();
Expand Down Expand Up @@ -307,6 +326,7 @@ public function testInsertingShouldPurgeSubresourceCollections(): void {

$this->uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert1]);
$this->uowProphecy->getScheduledEntityDeletions()->willReturn([]);
$this->uowProphecy->getScheduledEntityUpdates()->willReturn([])->shouldBeCalled();

// then
$this->cacheManagerProphecy->invalidateTags(['/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled();
Expand All @@ -326,8 +346,13 @@ public function testDeleteShouldPurgeSubresourceCollections(): void {
$relatedDummy->setId('100');
$toDelete1->setRelatedDummy($relatedDummy);

$this->uowProphecy->getScheduledEntityInsertions()->willReturn([]);
$this->uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete1]);
$uowMock = $this->createMock(UnitOfWork::class);
$uowMock->method('getScheduledEntityInsertions')->willReturn([]);
$uowMock->method('getScheduledEntityUpdates')->willReturn([]);
$uowMock->method('getScheduledEntityDeletions')->willReturn([$toDelete1]);
$uowMock->method('getEntityChangeSet')->willReturn([]);

$this->emProphecy->getUnitOfWork()->willReturn($uowMock)->shouldBeCalled();

// then
$this->cacheManagerProphecy->invalidateTags(['/dummies/1'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled();
Expand All @@ -339,4 +364,34 @@ public function testDeleteShouldPurgeSubresourceCollections(): void {
$listener->onFlush(new OnFlushEventArgs($this->emProphecy->reveal()));
$listener->postFlush();
}

public function testUpdateShouldPurgeSubresourceCollections(): void {
// given
$toUpdate1 = new Dummy();
$toUpdate1->setId('1');
$relatedDummy = new RelatedDummy();
$relatedDummy->setId('100');
$toUpdate1->setRelatedDummy($relatedDummy);

$relatedDummyOld = new RelatedDummy();
$relatedDummyOld->setId('99');

$uowMock = $this->createMock(UnitOfWork::class);
$uowMock->method('getScheduledEntityInsertions')->willReturn([]);
$uowMock->method('getScheduledEntityUpdates')->willReturn([$toUpdate1]);
$uowMock->method('getScheduledEntityDeletions')->willReturn([]);
$uowMock->method('getEntityChangeSet')->willReturn(['relatedDummy' => [$relatedDummyOld, $relatedDummy]]);

$this->emProphecy->getUnitOfWork()->willReturn($uowMock)->shouldBeCalled();

// then
$this->cacheManagerProphecy->invalidateTags(['/dummies/1'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled();
$this->cacheManagerProphecy->invalidateTags(['/related_dummies/100/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled();
$this->cacheManagerProphecy->invalidateTags(['/related_dummies/99/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled();

// when
$listener = new PurgeHttpCacheListener($this->iriConverterProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->propertyAccessorProphecy->reveal(), $this->metadataFactoryProphecy->reveal(), $this->cacheManagerProphecy->reveal());
$listener->onFlush(new OnFlushEventArgs($this->emProphecy->reveal()));
$listener->postFlush();
}
}

0 comments on commit 3f604db

Please sign in to comment.