From cd296b3115e56601c49766ccafc9a4d47ad7b8a7 Mon Sep 17 00:00:00 2001 From: jadams Date: Thu, 7 Aug 2025 15:34:27 +0200 Subject: [PATCH 01/40] Invalidate dependent elements --- config/services.php | 3 +- src/Element/InvalidateElementListener.php | 34 +++- .../Integration/Helpers/TestObjectFactory.php | 13 ++ .../Invalidation/InvalidateObjectTest.php | 26 +++ .../Element/InvalidateElementListenerTest.php | 57 ++++++ .../classes/definition_TestDataObject.php | 28 +++ .../pimcore/classes/definition_TestObject.php | 180 ++++++++++++++++++ 7 files changed, 338 insertions(+), 3 deletions(-) create mode 100644 tests/app/config/pimcore/classes/definition_TestObject.php diff --git a/config/services.php b/config/services.php index da95c24..792939c 100644 --- a/config/services.php +++ b/config/services.php @@ -91,7 +91,8 @@ $services->set('neusta_pimcore_http_cache.element.invalidate_listener', InvalidateElementListener::class) ->arg('$cacheInvalidator', service('neusta_pimcore_http_cache.cache_invalidator')) - ->arg('$dispatcher', service('event_dispatcher')); + ->arg('$dispatcher', service('event_dispatcher')) + ->arg('$elementRepository', service('.neusta_pimcore_http_cache.element.repository')); $services->set('neusta_pimcore_http_cache.data_collector', DataCollector::class) ->arg('$cacheTagCollector', service('.neusta_pimcore_http_cache.collect_tags_response_tagger')) diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index d8a2d8f..a7a21f7 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -4,6 +4,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheInvalidator; use Pimcore\Event\Model\ElementEventInterface; +use Pimcore\Model\Dependency; use Pimcore\Model\Element\ElementInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -12,6 +13,7 @@ final class InvalidateElementListener public function __construct( private readonly CacheInvalidator $cacheInvalidator, private readonly EventDispatcherInterface $dispatcher, + private readonly ElementRepository $elementRepository, ) { } @@ -21,12 +23,19 @@ public function onUpdate(ElementEventInterface $event): void return; } - $this->invalidateElement($event->getElement()); + $element = $event->getElement(); + + $this->invalidateElement($element); + + $this->invalidateDependencies($element->getDependencies()); } public function onDelete(ElementEventInterface $event): void { - $this->invalidateElement($event->getElement()); + $element = $event->getElement(); + + $this->invalidateElement($element); + $this->invalidateDependencies($element->getDependencies()); } private function invalidateElement(ElementInterface $element): void @@ -40,4 +49,25 @@ private function invalidateElement(ElementInterface $element): void $this->cacheInvalidator->invalidate($invalidationEvent->cacheTags()); } + + private function invalidateDependencies(Dependency $dependency): void + { + $requiredBy = $dependency->getRequiredBy(); + foreach ($requiredBy as $required) { + if (!isset($required['id'], $required['type'])) { + continue; + } + + $element = match (ElementType::tryFrom($required['type'])) { + ElementType::Object => $this->elementRepository->findObject($required['id']), + ElementType::Document => $this->elementRepository->findDocument($required['id']), + ElementType::Asset => $this->elementRepository->findAsset($required['id']), + default => null, + }; + + if ($element) { + $this->invalidateElement($element); + } + } + } } diff --git a/tests/Integration/Helpers/TestObjectFactory.php b/tests/Integration/Helpers/TestObjectFactory.php index 98d9f42..d605ed0 100644 --- a/tests/Integration/Helpers/TestObjectFactory.php +++ b/tests/Integration/Helpers/TestObjectFactory.php @@ -5,6 +5,7 @@ use Pimcore\Model\DataObject; use Pimcore\Model\DataObject\AbstractObject; use Pimcore\Model\DataObject\TestDataObject; +use Pimcore\Model\DataObject\TestObject; final class TestObjectFactory { @@ -20,6 +21,18 @@ public static function simpleObject(): TestDataObject return $object; } + public static function testObject(int $id): TestObject + { + $object = new TestObject(); + $object->setId($id); + $object->setKey('test_object_1' . $id); + $object->setContent('Test content'); + $object->setPublished(true); + $object->setParentId(1); + + return $object; + } + public static function simpleVariant(): TestDataObject { $object = new TestDataObject(); diff --git a/tests/Integration/Invalidation/InvalidateObjectTest.php b/tests/Integration/Invalidation/InvalidateObjectTest.php index a1c409f..76f9a24 100644 --- a/tests/Integration/Invalidation/InvalidateObjectTest.php +++ b/tests/Integration/Invalidation/InvalidateObjectTest.php @@ -55,6 +55,32 @@ public function response_is_invalidated_when_object_is_updated(): void $this->cacheManager->invalidateTags(['o42'])->shouldHaveBeenCalledTimes(1); } + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => [ + 'enabled' => true, + 'types' => [ + 'variant' => true, + ], + ], + ], + ])] + public function dependent_element_is_invalidated_on_update(): void + { + $object1 = self::arrange(fn () => TestObjectFactory::testObject(12)->save()); + $object2 = self::arrange(fn () => TestObjectFactory::testObject(24)->save()); + + self::arrange(fn () => $object1->setRelated([$object2])->save()); + + $object2->setContent('Updated content')->save(); + + $this->cacheManager->invalidateTags(['o12'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags(['o24'])->shouldHaveBeenCalledTimes(1); + } + /** * @test */ diff --git a/tests/Unit/Element/InvalidateElementListenerTest.php b/tests/Unit/Element/InvalidateElementListenerTest.php index 21d541b..eeed689 100644 --- a/tests/Unit/Element/InvalidateElementListenerTest.php +++ b/tests/Unit/Element/InvalidateElementListenerTest.php @@ -6,6 +6,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTags; use Neusta\Pimcore\HttpCacheBundle\Element\ElementInvalidationEvent; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener; use PHPUnit\Framework\TestCase; use Pimcore\Event\Model\AssetEvent; @@ -14,6 +15,7 @@ use Pimcore\Event\Model\ElementEventInterface; use Pimcore\Model\Asset; use Pimcore\Model\DataObject; +use Pimcore\Model\Dependency; use Pimcore\Model\Document; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -32,13 +34,18 @@ final class InvalidateElementListenerTest extends TestCase /** @var ObjectProphecy */ private $eventDispatcher; + /** @var ObjectProphecy */ + private $elementRepository; + protected function setUp(): void { $this->cacheInvalidator = $this->prophesize(CacheInvalidator::class); $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $this->elementRepository = $this->prophesize(ElementRepository::class); $this->invalidateElementListener = new InvalidateElementListener( $this->cacheInvalidator->reveal(), $this->eventDispatcher->reveal(), + $this->elementRepository->reveal(), ); $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) @@ -105,6 +112,28 @@ public function onUpdate_should_invalidate_elements(ElementEventInterface $event ->shouldHaveBeenCalledOnce(); } + /** + * @test + */ + public function onUpdate_should_invalidate_dependent_elements(): void + { + $element = $this->prophesize(DataObject\TestDataObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentElement = $this->prophesize(DataObject::class); + $event = new DataObjectEvent($element->reveal()); + + $element->getId()->willReturn(42); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentElement->getId()->willReturn(23); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + $this->elementRepository->findObject(23)->willReturn($dependentElement->reveal()); + + $this->invalidateElementListener->onUpdate($event); + + $this->cacheInvalidator->invalidate(Argument::which('toString', CacheTag::fromElement($dependentElement->reveal())->toString())) + ->shouldHaveBeenCalledOnce(); + } + /** * @test * @@ -177,6 +206,28 @@ public function onDelete_should_invalidate_elements(ElementEventInterface $event ->shouldHaveBeenCalledOnce(); } + /** + * @test + */ + public function onDelete_should_invalidate_dependent_elements(): void + { + $element = $this->prophesize(DataObject\TestDataObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentElement = $this->prophesize(DataObject::class); + $event = new DataObjectEvent($element->reveal()); + + $element->getId()->willReturn(42); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentElement->getId()->willReturn(23); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + $this->elementRepository->findObject(23)->willReturn($dependentElement->reveal()); + + $this->invalidateElementListener->onDelete($event); + + $this->cacheInvalidator->invalidate(Argument::which('toString', CacheTag::fromElement($dependentElement->reveal())->toString())) + ->shouldHaveBeenCalledOnce(); + } + /** * @test * @@ -223,16 +274,22 @@ public function onDelete_should_invalidate_additional_tags_when_requested(Elemen public function elementProvider(): iterable { + $dependency = $this->prophesize(Dependency::class); + $asset = $this->prophesize(Asset::class); $asset->getId()->willReturn(42); + $asset->getDependencies()->willReturn($dependency->reveal()); yield 'Asset' => ['event' => new AssetEvent($asset->reveal())]; $document = $this->prophesize(Document::class); $document->getId()->willReturn(42); + $document->getDependencies()->willReturn($dependency->reveal()); yield 'Document' => ['event' => new DocumentEvent($document->reveal())]; $dataObject = $this->prophesize(DataObject::class); $dataObject->getId()->willReturn(42); + $dataObject->getDependencies()->willReturn($dependency->reveal()); + $dependency->getRequiredBy()->willReturn([]); yield 'Object' => ['event' => new DataObjectEvent($dataObject->reveal())]; } } diff --git a/tests/app/config/pimcore/classes/definition_TestDataObject.php b/tests/app/config/pimcore/classes/definition_TestDataObject.php index c7ed3b8..b789edd 100644 --- a/tests/app/config/pimcore/classes/definition_TestDataObject.php +++ b/tests/app/config/pimcore/classes/definition_TestDataObject.php @@ -76,6 +76,34 @@ 'showCharCount' => false, 'defaultValueGenerator' => '', ]), + 1 => Pimcore\Model\DataObject\ClassDefinition\Data\ManyToOneRelation::__set_state([ + 'name' => 'relatedObjects', + 'title' => 'Related Objects', + 'tooltip' => '', + 'mandatory' => false, + 'noteditable' => false, + 'index' => false, + 'locked' => false, + 'style' => '', + 'permissions' => null, + 'datatype' => 'data', + 'invisible' => false, + 'visibleGridView' => false, + 'visibleSearch' => false, + 'classes' => [ + 0 => [ + 'classes' => 'TestDataObject', + ], + ], + 'pathFormatterClass' => '', + 'width' => '', + 'assetUploadPath' => '', + 'objectsAllowed' => true, + 'assetsAllowed' => false, + 'assetTypes' => [], + 'documentsAllowed' => false, + 'documentTypes' => [], + ]), ], 'locked' => false, 'blockedVarsForExport' => [ diff --git a/tests/app/config/pimcore/classes/definition_TestObject.php b/tests/app/config/pimcore/classes/definition_TestObject.php new file mode 100644 index 0000000..294d396 --- /dev/null +++ b/tests/app/config/pimcore/classes/definition_TestObject.php @@ -0,0 +1,180 @@ + null, + 'id' => 'test_object', + 'name' => 'TestObject', + 'description' => '', + 'creationDate' => 0, + 'modificationDate' => 1754659392, + 'userOwner' => 58, + 'userModification' => 58, + 'parentClass' => '', + 'implementsInterfaces' => '', + 'listingParentClass' => '', + 'useTraits' => '', + 'listingUseTraits' => '', + 'encryption' => false, + 'encryptedTables' => [ + ], + 'allowInherit' => false, + 'allowVariants' => false, + 'showVariants' => false, + 'fieldDefinitions' => [ + ], + 'layoutDefinitions' => Pimcore\Model\DataObject\ClassDefinition\Layout\Panel::__set_state([ + 'name' => 'pimcore_root', + 'type' => null, + 'region' => null, + 'title' => null, + 'width' => 0, + 'height' => 0, + 'collapsible' => false, + 'collapsed' => false, + 'bodyStyle' => null, + 'datatype' => 'layout', + 'permissions' => null, + 'children' => [ + 0 => Pimcore\Model\DataObject\ClassDefinition\Layout\Panel::__set_state([ + 'name' => 'Layout', + 'type' => null, + 'region' => null, + 'title' => '', + 'width' => '', + 'height' => '', + 'collapsible' => false, + 'collapsed' => false, + 'bodyStyle' => '', + 'datatype' => 'layout', + 'permissions' => null, + 'children' => [ + 0 => Pimcore\Model\DataObject\ClassDefinition\Data\Input::__set_state([ + 'name' => 'content', + 'title' => 'Content', + 'tooltip' => '', + 'mandatory' => false, + 'noteditable' => false, + 'index' => false, + 'locked' => false, + 'style' => '', + 'permissions' => null, + 'datatype' => 'data', + 'fieldtype' => 'input', + 'relationType' => false, + 'invisible' => false, + 'visibleGridView' => false, + 'visibleSearch' => false, + 'blockedVarsForExport' => [ + ], + 'width' => '', + 'defaultValue' => null, + 'columnLength' => 190, + 'regex' => '', + 'regexFlags' => [ + ], + 'unique' => false, + 'showCharCount' => false, + 'defaultValueGenerator' => '', + ]), + 1 => Pimcore\Model\DataObject\ClassDefinition\Data\ManyToManyObjectRelation::__set_state([ + 'name' => 'related', + 'title' => 'Related', + 'tooltip' => '', + 'mandatory' => false, + 'noteditable' => false, + 'index' => false, + 'locked' => false, + 'style' => '', + 'permissions' => null, + 'datatype' => 'data', + 'fieldtype' => 'manyToManyObjectRelation', + 'relationType' => true, + 'invisible' => false, + 'visibleGridView' => false, + 'visibleSearch' => false, + 'blockedVarsForExport' => [ + ], + 'classes' => [ + 0 => [ + 'classes' => 'TestObject', + ], + ], + 'pathFormatterClass' => '', + 'width' => '', + 'height' => '', + 'maxItems' => null, + 'visibleFields' => [ + ], + 'allowToCreateNewObject' => false, + 'optimizedAdminLoading' => false, + 'enableTextSelection' => false, + 'visibleFieldDefinitions' => [ + ], + ]), + ], + 'locked' => false, + 'blockedVarsForExport' => [ + ], + 'fieldtype' => 'panel', + 'layout' => null, + 'border' => false, + 'icon' => '', + 'labelWidth' => 0, + 'labelAlign' => 'left', + ]), + ], + 'locked' => false, + 'blockedVarsForExport' => [ + ], + 'fieldtype' => 'panel', + 'layout' => null, + 'border' => false, + 'icon' => null, + 'labelWidth' => 100, + 'labelAlign' => 'left', + ]), + 'icon' => '', + 'previewUrl' => '', + 'group' => '', + 'showAppLoggerTab' => false, + 'linkGeneratorReference' => '', + 'previewGeneratorReference' => '', + 'compositeIndices' => [ + ], + 'generateTypeDeclarations' => true, + 'showFieldLookup' => false, + 'propertyVisibility' => [ + 'grid' => [ + 'id' => true, + 'key' => false, + 'path' => true, + 'published' => true, + 'modificationDate' => true, + 'creationDate' => true, + ], + 'search' => [ + 'id' => true, + 'key' => false, + 'path' => true, + 'published' => true, + 'modificationDate' => true, + 'creationDate' => true, + ], + ], + 'enableGridLocking' => false, + 'deletedDataComponents' => [ + ], + 'blockedVarsForExport' => [ + ], + 'activeDispatchingEvents' => [ + ], +]); From 70c0a27592b5f8f2160b963874c0282627210bea Mon Sep 17 00:00:00 2001 From: jadams Date: Tue, 12 Aug 2025 16:14:00 +0200 Subject: [PATCH 02/40] Refactor test factories --- .../CollectConfigurationDataTest.php | 4 +- .../Integration/Helpers/TestAssetFactory.php | 18 ++++----- .../Helpers/TestDocumentFactory.php | 30 +++++++------- .../Integration/Helpers/TestObjectFactory.php | 32 ++++++--------- .../Invalidation/CancelInvalidationTest.php | 12 +++--- .../InvalidateAdditionalTagTest.php | 40 +++++++++---------- .../Invalidation/InvalidateAssetTest.php | 10 ++--- .../Invalidation/InvalidateDocumentTest.php | 12 +++--- .../Invalidation/InvalidateObjectTest.php | 26 ++++++------ .../Tagging/TagAdditionalTagTest.php | 32 +++++++-------- tests/Integration/Tagging/TagAssetTest.php | 22 +++++----- tests/Integration/Tagging/TagDocumentTest.php | 36 ++++++++--------- tests/Integration/Tagging/TagObjectTest.php | 32 +++++++-------- 13 files changed, 148 insertions(+), 158 deletions(-) diff --git a/tests/Integration/Configuration/CollectConfigurationDataTest.php b/tests/Integration/Configuration/CollectConfigurationDataTest.php index d0e1870..32edb6a 100644 --- a/tests/Integration/Configuration/CollectConfigurationDataTest.php +++ b/tests/Integration/Configuration/CollectConfigurationDataTest.php @@ -40,7 +40,7 @@ protected function setUp(): void ])] public function collects_configuration_data(): void { - self::arrange(fn () => TestDocumentFactory::simplePage())->save(); + self::arrange(fn () => TestDocumentFactory::simplePage(5))->save(); $this->client->request('GET', '/test_document_page'); $this->client->enableProfiler(); @@ -65,7 +65,7 @@ public function collects_configuration_data(): void ])] public function does_not_collect_configuration_data_when_profiler_is_disabled(): void { - self::arrange(fn () => TestDocumentFactory::simplePage())->save(); + self::arrange(fn () => TestDocumentFactory::simplePage(5))->save(); $this->client->request('GET', '/test_document_page'); $this->client->enableProfiler(); diff --git a/tests/Integration/Helpers/TestAssetFactory.php b/tests/Integration/Helpers/TestAssetFactory.php index 5e075e9..168b82c 100644 --- a/tests/Integration/Helpers/TestAssetFactory.php +++ b/tests/Integration/Helpers/TestAssetFactory.php @@ -6,11 +6,11 @@ final class TestAssetFactory { - public static function simpleAsset(): Asset + public static function simpleAsset(int $id, string $fileName = 'test-asset.txt'): Asset { $asset = new Asset(); - $asset->setId(42); - $asset->setFilename('test-asset.txt'); + $asset->setId($id); + $asset->setFilename($fileName); $asset->setParentId(1); $asset->setData('This is the content of the test asset.'); $asset->setMimetype('text/plain'); @@ -18,22 +18,22 @@ public static function simpleAsset(): Asset return $asset; } - public static function simpleImage(): Asset\Image + public static function simpleImage(int $id, string $fileName = 'test-asset.jpg'): Asset\Image { $image = new Asset\Image(); - $image->setId(17); - $image->setFilename('test-asset.jpg'); + $image->setId($id); + $image->setFilename($fileName); $image->setParentId(1); $image->setMimetype('image/jpeg'); return $image; } - public static function simpleFolder(): Asset\Folder + public static function simpleFolder(int $id, string $key = 'test-asset-folder'): Asset\Folder { $folder = new Asset\Folder(); - $folder->setKey('test-asset-folder'); - $folder->setId(23); + $folder->setId($id); + $folder->setKey($key); $folder->setParentId(1); return $folder; diff --git a/tests/Integration/Helpers/TestDocumentFactory.php b/tests/Integration/Helpers/TestDocumentFactory.php index 7f7c4eb..26c1398 100644 --- a/tests/Integration/Helpers/TestDocumentFactory.php +++ b/tests/Integration/Helpers/TestDocumentFactory.php @@ -10,55 +10,55 @@ final class TestDocumentFactory { - public static function simplePage(): Page + public static function simplePage(int $id, string $key = 'test_document_page'): Page { $page = new Page(); - $page->setId(42); - $page->setKey('test_document_page'); + $page->setId($id); + $page->setKey($key); $page->setPublished(true); $page->setParentId(1); return $page; } - public static function simpleSnippet(): Snippet + public static function simpleSnippet(int $id, string $key = 'test_document_snippet'): Snippet { $snippet = new Snippet(); - $snippet->setId(23); - $snippet->setKey('test_document_snippet'); + $snippet->setId($id); + $snippet->setKey($key); $snippet->setPublished(true); $snippet->setParentId(1); return $snippet; } - public static function simpleEmail(): Email + public static function simpleEmail(int $id, string $key = 'test_document_email'): Email { $email = new Email(); - $email->setId(17); - $email->setKey('test_document_link'); + $email->setId($id); + $email->setKey($key); $email->setPublished(true); $email->setParentId(1); return $email; } - public static function simpleHardLink(): Hardlink + public static function simpleHardLink(int $id, string $key = 'test_document_hard_link'): Hardlink { $hardlink = new Hardlink(); - $hardlink->setId(33); - $hardlink->setKey('test_document_hard_link'); + $hardlink->setId($id); + $hardlink->setKey($key); $hardlink->setPublished(true); $hardlink->setParentId(1); return $hardlink; } - public static function simpleFolder(): Folder + public static function simpleFolder(int $id, string $key = 'test_document_folder'): Folder { $folder = new Folder(); - $folder->setId(97); - $folder->setKey('test_document_folder'); + $folder->setId($id); + $folder->setKey($key); $folder->setPublished(true); $folder->setParentId(1); diff --git a/tests/Integration/Helpers/TestObjectFactory.php b/tests/Integration/Helpers/TestObjectFactory.php index d605ed0..70aec12 100644 --- a/tests/Integration/Helpers/TestObjectFactory.php +++ b/tests/Integration/Helpers/TestObjectFactory.php @@ -9,35 +9,27 @@ final class TestObjectFactory { - public static function simpleObject(): TestDataObject - { - $object = new TestDataObject(); - $object->setId(42); - $object->setKey('test_object'); - $object->setContent('Test content'); - $object->setPublished(true); - $object->setParentId(1); - - return $object; - } - - public static function testObject(int $id): TestObject + /** + * @param list $related + */ + public static function simpleObject(int $id, string $key = 'test_object', array $related = []): TestObject { $object = new TestObject(); $object->setId($id); - $object->setKey('test_object_1' . $id); + $object->setKey($key); $object->setContent('Test content'); + $object->setRelated($related); $object->setPublished(true); $object->setParentId(1); return $object; } - public static function simpleVariant(): TestDataObject + public static function simpleVariant(int $id, string $key = 'simple_variant'): TestDataObject { $object = new TestDataObject(); - $object->setId(17); - $object->setKey('test_variant'); + $object->setId($id); + $object->setKey($key); $object->setContent('Test content'); $object->setPublished(true); $object->setParentId(1); @@ -46,11 +38,11 @@ public static function simpleVariant(): TestDataObject return $object; } - public static function simpleFolder(): DataObject\Folder + public static function simpleFolder(int $id, string $key = 'simple_folder'): DataObject\Folder { $folder = new DataObject\Folder(); - $folder->setId(23); - $folder->setKey('test_folder'); + $folder->setId($id); + $folder->setKey($key); $folder->setParentId(1); return $folder; diff --git a/tests/Integration/Invalidation/CancelInvalidationTest.php b/tests/Integration/Invalidation/CancelInvalidationTest.php index ec9364c..5462afe 100644 --- a/tests/Integration/Invalidation/CancelInvalidationTest.php +++ b/tests/Integration/Invalidation/CancelInvalidationTest.php @@ -45,7 +45,7 @@ protected function setUp(): void ])] public function cancel_invalidation_on_object_update(): void { - $object = self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + $object = self::arrange(fn () => TestObjectFactory::simpleObject(42)->save()); $object->setContent('Updated test content')->save(); @@ -62,7 +62,7 @@ public function cancel_invalidation_on_object_update(): void ])] public function cancel_invalidation_on_document_update(): void { - $document = self::arrange(fn () => TestDocumentFactory::simplePage()->save()); + $document = self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $document->setKey('updated_test_document_page')->save(); @@ -79,7 +79,7 @@ public function cancel_invalidation_on_document_update(): void ])] public function cancel_invalidation_on_asset_update(): void { - $asset = self::arrange(fn () => TestAssetFactory::simpleAsset()->save()); + $asset = self::arrange(fn () => TestAssetFactory::simpleAsset(5)->save()); $asset->setData('Updated test content')->save(); @@ -96,7 +96,7 @@ public function cancel_invalidation_on_asset_update(): void ])] public function cancel_invalidation_on_object_delete(): void { - $object = self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + $object = self::arrange(fn () => TestObjectFactory::simpleObject(42)->save()); $object->delete(); @@ -113,7 +113,7 @@ public function cancel_invalidation_on_object_delete(): void ])] public function cancel_invalidation_on_document_delete(): void { - $document = self::arrange(fn () => TestDocumentFactory::simplePage()->save()); + $document = self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $document->delete(); @@ -130,7 +130,7 @@ public function cancel_invalidation_on_document_delete(): void ])] public function cancel_invalidation_on_asset_delete(): void { - $asset = self::arrange(fn () => TestAssetFactory::simpleAsset()->save()); + $asset = self::arrange(fn () => TestAssetFactory::simpleAsset(5)->save()); $asset->delete(); diff --git a/tests/Integration/Invalidation/InvalidateAdditionalTagTest.php b/tests/Integration/Invalidation/InvalidateAdditionalTagTest.php index 55f8662..402af43 100644 --- a/tests/Integration/Invalidation/InvalidateAdditionalTagTest.php +++ b/tests/Integration/Invalidation/InvalidateAdditionalTagTest.php @@ -53,7 +53,7 @@ protected function setUp(): void ])] public function invalidate_additional_tag_on_object_update(): void { - $object = self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + $object = self::arrange(fn () => TestObjectFactory::simpleObject(42)->save()); $object->setContent('Updated test content')->save(); @@ -73,7 +73,7 @@ public function invalidate_additional_tag_on_object_update(): void ])] public function does_not_invalidate_additional_tag_on_object_update_when_cache_type_is_disabled(): void { - $object = self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + $object = self::arrange(fn () => TestObjectFactory::simpleObject(42)->save()); $object->setKey('updated_test_object')->save(); @@ -93,11 +93,11 @@ public function does_not_invalidate_additional_tag_on_object_update_when_cache_t ])] public function invalidate_additional_tag_on_document_update(): void { - $document = self::arrange(fn () => TestDocumentFactory::simplePage()->save()); + $document = self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $document->setKey('updated_test_document_page')->save(); - $this->cacheManager->invalidateTags(['d42', 'foo-bar'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags(['d5', 'foo-bar'])->shouldHaveBeenCalledTimes(1); } /** @@ -113,11 +113,11 @@ public function invalidate_additional_tag_on_document_update(): void ])] public function does_not_invalidate_additional_tag_on_document_update_when_cache_type_is_disabled(): void { - $document = self::arrange(fn () => TestDocumentFactory::simplePage()->save()); + $document = self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $document->setKey('updated_test_document_page')->save(); - $this->cacheManager->invalidateTags(['d42', 'foo-bar'])->shouldNotHaveBeenCalled(); + $this->cacheManager->invalidateTags(['d5', 'foo-bar'])->shouldNotHaveBeenCalled(); } /** @@ -133,11 +133,11 @@ public function does_not_invalidate_additional_tag_on_document_update_when_cache ])] public function invalidate_additional_tag_on_asset_update(): void { - $asset = self::arrange(fn () => TestAssetFactory::simpleAsset()->save()); + $asset = self::arrange(fn () => TestAssetFactory::simpleAsset(5)->save()); $asset->setData('Updated test content')->save(); - $this->cacheManager->invalidateTags(['a42', 'foo-bar'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags(['a5', 'foo-bar'])->shouldHaveBeenCalledTimes(1); } /** @@ -153,11 +153,11 @@ public function invalidate_additional_tag_on_asset_update(): void ])] public function does_not_invalidate_additional_tag_on_asset_update_when_cache_type_is_disabled(): void { - $asset = self::arrange(fn () => TestAssetFactory::simpleAsset()->save()); + $asset = self::arrange(fn () => TestAssetFactory::simpleAsset(5)->save()); $asset->setData('Updated test content')->save(); - $this->cacheManager->invalidateTags(['a42', 'foo-bar'])->shouldNotHaveBeenCalled(); + $this->cacheManager->invalidateTags(['a5', 'foo-bar'])->shouldNotHaveBeenCalled(); } /** @@ -173,7 +173,7 @@ public function does_not_invalidate_additional_tag_on_asset_update_when_cache_ty ])] public function invalidate_additional_tag_on_object_deletion(): void { - $object = self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + $object = self::arrange(fn () => TestObjectFactory::simpleObject(42)->save()); $object->delete(); @@ -193,7 +193,7 @@ public function invalidate_additional_tag_on_object_deletion(): void ])] public function does_not_invalidate_additional_tag_on_object_deletion_when_cache_type_is_disabled(): void { - $object = self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + $object = self::arrange(fn () => TestObjectFactory::simpleObject(42)->save()); $object->delete(); @@ -213,11 +213,11 @@ public function does_not_invalidate_additional_tag_on_object_deletion_when_cache ])] public function invalidate_additional_tag_on_asset_deletion(): void { - $asset = self::arrange(fn () => TestAssetFactory::simpleAsset()->save()); + $asset = self::arrange(fn () => TestAssetFactory::simpleAsset(5)->save()); $asset->delete(); - $this->cacheManager->invalidateTags(['a42', 'foo-bar'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags(['a5', 'foo-bar'])->shouldHaveBeenCalledTimes(1); } /** @@ -233,11 +233,11 @@ public function invalidate_additional_tag_on_asset_deletion(): void ])] public function does_not_invalidate_additional_tag_on_asset_deletion_when_cache_type_is_disabled(): void { - $asset = self::arrange(fn () => TestAssetFactory::simpleAsset()->save()); + $asset = self::arrange(fn () => TestAssetFactory::simpleAsset(5)->save()); $asset->delete(); - $this->cacheManager->invalidateTags(['a42', 'foo-bar'])->shouldNotHaveBeenCalled(); + $this->cacheManager->invalidateTags(['a5', 'foo-bar'])->shouldNotHaveBeenCalled(); } /** @@ -253,11 +253,11 @@ public function does_not_invalidate_additional_tag_on_asset_deletion_when_cache_ ])] public function invalidate_additional_tag_on_document_deletion(): void { - $document = self::arrange(fn () => TestDocumentFactory::simplePage()->save()); + $document = self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $document->delete(); - $this->cacheManager->invalidateTags(['d42', 'foo-bar'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags(['d5', 'foo-bar'])->shouldHaveBeenCalledTimes(1); } /** @@ -273,10 +273,10 @@ public function invalidate_additional_tag_on_document_deletion(): void ])] public function does_not_invalidate_additional_tag_on_document_deletion_when_cache_type_was_disabled(): void { - $document = self::arrange(fn () => TestDocumentFactory::simplePage()->save()); + $document = self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $document->delete(); - $this->cacheManager->invalidateTags(['d42', 'foo-bar'])->shouldNotHaveBeenCalled(); + $this->cacheManager->invalidateTags(['d5', 'foo-bar'])->shouldNotHaveBeenCalled(); } } diff --git a/tests/Integration/Invalidation/InvalidateAssetTest.php b/tests/Integration/Invalidation/InvalidateAssetTest.php index e66430d..cd26b65 100644 --- a/tests/Integration/Invalidation/InvalidateAssetTest.php +++ b/tests/Integration/Invalidation/InvalidateAssetTest.php @@ -34,9 +34,9 @@ protected function setUp(): void $this->cacheManager->invalidateTags(Argument::any())->willReturn($this->cacheManager->reveal()); self::getContainer()->set('fos_http_cache.cache_manager', $this->cacheManager->reveal()); - $this->asset = self::arrange(fn () => TestAssetFactory::simpleAsset()->save()); - $this->folder = self::arrange(fn () => TestAssetFactory::simpleFolder()->save()); - $this->image = self::arrange(fn () => TestAssetFactory::simpleImage()->save()); + $this->asset = self::arrange(fn () => TestAssetFactory::simpleAsset(5)->save()); + $this->folder = self::arrange(fn () => TestAssetFactory::simpleFolder(12)->save()); + $this->image = self::arrange(fn () => TestAssetFactory::simpleImage(29)->save()); } /** @@ -51,7 +51,7 @@ public function response_is_invalidated_when_asset_is_updated(): void { $this->asset->setData('Updated test content')->save(); - $this->cacheManager->invalidateTags(['a42'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags(['a5'])->shouldHaveBeenCalledTimes(1); } /** @@ -66,7 +66,7 @@ public function response_is_invalidated_when_asset_is_deleted(): void { $this->asset->delete(); - $this->cacheManager->invalidateTags(['a42'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags(['a5'])->shouldHaveBeenCalledTimes(1); } /** diff --git a/tests/Integration/Invalidation/InvalidateDocumentTest.php b/tests/Integration/Invalidation/InvalidateDocumentTest.php index 1618cbe..5837cc3 100644 --- a/tests/Integration/Invalidation/InvalidateDocumentTest.php +++ b/tests/Integration/Invalidation/InvalidateDocumentTest.php @@ -36,10 +36,10 @@ protected function setUp(): void $this->cacheManager->invalidateTags(Argument::any())->willReturn($this->cacheManager->reveal()); self::getContainer()->set('fos_http_cache.cache_manager', $this->cacheManager->reveal()); - $this->document = self::arrange(fn () => TestDocumentFactory::simplePage()->save()); - $this->hardlink = self::arrange(fn () => TestDocumentFactory::simpleHardLink()->save()); - $this->email = self::arrange(fn () => TestDocumentFactory::simpleEmail()->save()); - $this->folder = self::arrange(fn () => TestDocumentFactory::simpleFolder()->save()); + $this->document = self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); + $this->hardlink = self::arrange(fn () => TestDocumentFactory::simpleHardLink(12)->save()); + $this->email = self::arrange(fn () => TestDocumentFactory::simpleEmail(29)->save()); + $this->folder = self::arrange(fn () => TestDocumentFactory::simpleFolder(70)->save()); } /** @@ -54,7 +54,7 @@ public function response_is_invalidated_when_document_is_updated(): void { $this->document->setKey('updated_test_document_page')->save(); - $this->cacheManager->invalidateTags(['d42'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags(['d5'])->shouldHaveBeenCalledTimes(1); } /** @@ -69,7 +69,7 @@ public function response_is_invalidated_when_document_is_deleted(): void { $this->document->delete(); - $this->cacheManager->invalidateTags(['d42'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags(['d5'])->shouldHaveBeenCalledTimes(1); } /** diff --git a/tests/Integration/Invalidation/InvalidateObjectTest.php b/tests/Integration/Invalidation/InvalidateObjectTest.php index 76f9a24..b5e6c70 100644 --- a/tests/Integration/Invalidation/InvalidateObjectTest.php +++ b/tests/Integration/Invalidation/InvalidateObjectTest.php @@ -10,6 +10,7 @@ use Neusta\Pimcore\TestingFramework\Test\ConfigurableKernelTestCase; use Pimcore\Model\DataObject; use Pimcore\Model\DataObject\TestDataObject; +use Pimcore\Model\DataObject\TestObject; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; @@ -23,7 +24,9 @@ final class InvalidateObjectTest extends ConfigurableKernelTestCase /** @var ObjectProphecy */ private ObjectProphecy $cacheManager; - private TestDataObject $object; + private TestObject $object; + + private TestObject $otherObject; private TestDataObject $variant; @@ -34,10 +37,10 @@ protected function setUp(): void $this->cacheManager = $this->prophesize(CacheManager::class); $this->cacheManager->invalidateTags(Argument::any())->willReturn($this->cacheManager->reveal()); self::getContainer()->set('fos_http_cache.cache_manager', $this->cacheManager->reveal()); - - $this->object = self::arrange(fn () => TestObjectFactory::simpleObject()->save()); - $this->folder = self::arrange(fn () => TestObjectFactory::simpleFolder()->save()); - $this->variant = self::arrange(fn () => TestObjectFactory::simpleVariant()->save()); + $this->object = self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); + $this->otherObject = self::arrange(fn () => TestObjectFactory::simpleObject(12, 'other_test_object', [$this->object])->save()); + $this->folder = self::arrange(fn () => TestObjectFactory::simpleFolder(29)->save()); + $this->variant = self::arrange(fn () => TestObjectFactory::simpleVariant(70)->save()); } /** @@ -52,7 +55,7 @@ public function response_is_invalidated_when_object_is_updated(): void { $this->object->setContent('Updated test content')->save(); - $this->cacheManager->invalidateTags(['o42'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags(['o5'])->shouldHaveBeenCalledTimes(1); } /** @@ -70,15 +73,10 @@ public function response_is_invalidated_when_object_is_updated(): void ])] public function dependent_element_is_invalidated_on_update(): void { - $object1 = self::arrange(fn () => TestObjectFactory::testObject(12)->save()); - $object2 = self::arrange(fn () => TestObjectFactory::testObject(24)->save()); - - self::arrange(fn () => $object1->setRelated([$object2])->save()); - - $object2->setContent('Updated content')->save(); + $this->otherObject->setContent('Updated content')->save(); + $this->cacheManager->invalidateTags(['o5'])->shouldHaveBeenCalledTimes(1); $this->cacheManager->invalidateTags(['o12'])->shouldHaveBeenCalledTimes(1); - $this->cacheManager->invalidateTags(['o24'])->shouldHaveBeenCalledTimes(1); } /** @@ -93,7 +91,7 @@ public function response_is_invalidated_when_object_is_deleted(): void { $this->object->delete(); - $this->cacheManager->invalidateTags(['o42'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags(['o5'])->shouldHaveBeenCalledTimes(1); } /** diff --git a/tests/Integration/Tagging/TagAdditionalTagTest.php b/tests/Integration/Tagging/TagAdditionalTagTest.php index 5abf34f..7c14bf8 100644 --- a/tests/Integration/Tagging/TagAdditionalTagTest.php +++ b/tests/Integration/Tagging/TagAdditionalTagTest.php @@ -44,24 +44,24 @@ protected function setUp(): void ])] public function response_is_tagged_with_additional_tag_when_asset_is_loaded(): void { - self::arrange(fn () => TestAssetFactory::simpleAsset()->save()); - self::arrange(fn () => TestAssetFactory::simpleImage()->save()); + self::arrange(fn () => TestAssetFactory::simpleAsset(5)->save()); + self::arrange(fn () => TestAssetFactory::simpleImage(29)->save()); self::getContainer()->get('event_dispatcher')->addListener( ElementTaggingEvent::class, fn (ElementTaggingEvent $event) => $event->addTag( - CacheTag::fromString('17', new ElementCacheType(ElementType::Asset)), + CacheTag::fromString('29', new ElementCacheType(ElementType::Asset)), ), ); - $this->client->request('GET', '/get-asset?id=42'); + $this->client->request('GET', '/get-asset?id=5'); $response = $this->client->getResponse(); self::assertSame('This is the content of the test asset.', $response->getContent()); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringContainsString('a17', $response->headers->get('X-Cache-Tags')); + self::assertStringContainsString('a29', $response->headers->get('X-Cache-Tags')); } /** @@ -74,13 +74,13 @@ public function response_is_tagged_with_additional_tag_when_asset_is_loaded(): v ])] public function response_is_tagged_with_additional_tag_when_document_is_loaded(): void { - self::arrange(fn () => TestDocumentFactory::simplePage()->save()); - self::arrange(fn () => TestDocumentFactory::simpleSnippet()->save()); + self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); + self::arrange(fn () => TestDocumentFactory::simpleSnippet(12)->save()); self::getContainer()->get('event_dispatcher')->addListener( ElementTaggingEvent::class, fn (ElementTaggingEvent $event) => $event->addTag( - CacheTag::fromString('23', new ElementCacheType(ElementType::Document)), + CacheTag::fromString('12', new ElementCacheType(ElementType::Document)), ), ); @@ -91,7 +91,7 @@ public function response_is_tagged_with_additional_tag_when_document_is_loaded() self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringContainsString('d23', $response->headers->get('X-Cache-Tags')); + self::assertStringContainsString('d12', $response->headers->get('X-Cache-Tags')); } /** @@ -104,24 +104,24 @@ public function response_is_tagged_with_additional_tag_when_document_is_loaded() ])] public function response_is_tagged_with_additional_tag_when_object_is_loaded(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); - self::arrange(fn () => TestObjectFactory::simpleVariant()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); + self::arrange(fn () => TestObjectFactory::simpleVariant(12)->save()); self::getContainer()->get('event_dispatcher')->addListener( ElementTaggingEvent::class, fn (ElementTaggingEvent $event) => $event->addTag( - CacheTag::fromString('17', new ElementCacheType(ElementType::Object)), + CacheTag::fromString('12', new ElementCacheType(ElementType::Object)), ), ); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $response = $this->client->getResponse(); self::assertSame('Test content', $response->getContent()); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringContainsString('o17', $response->headers->get('X-Cache-Tags')); + self::assertStringContainsString('o5', $response->headers->get('X-Cache-Tags')); } /** @@ -137,7 +137,7 @@ public function response_is_tagged_with_additional_tag_when_object_is_loaded(): ])] public function response_is_tagged_with_custom_tag_when_element_is_loaded(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); self::getContainer()->get('event_dispatcher')->addListener( ElementTaggingEvent::class, @@ -146,7 +146,7 @@ public function response_is_tagged_with_custom_tag_when_element_is_loaded(): voi ), ); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $response = $this->client->getResponse(); self::assertSame('Test content', $response->getContent()); diff --git a/tests/Integration/Tagging/TagAssetTest.php b/tests/Integration/Tagging/TagAssetTest.php index d1aa49a..b036434 100644 --- a/tests/Integration/Tagging/TagAssetTest.php +++ b/tests/Integration/Tagging/TagAssetTest.php @@ -34,16 +34,16 @@ protected function setUp(): void ])] public function response_is_tagged_with_expected_tags_when_asset_is_loaded(): void { - self::arrange(fn () => TestAssetFactory::simpleAsset()->save()); + self::arrange(fn () => TestAssetFactory::simpleAsset(5)->save()); - $this->client->request('GET', '/get-asset?id=42'); + $this->client->request('GET', '/get-asset?id=5'); $response = $this->client->getResponse(); self::assertSame('This is the content of the test asset.', $response->getContent()); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertSame('a42', $response->headers->get('X-Cache-Tags')); + self::assertSame('a5', $response->headers->get('X-Cache-Tags')); } /** @@ -56,9 +56,9 @@ public function response_is_tagged_with_expected_tags_when_asset_is_loaded(): vo ])] public function response_is_not_tagged_when_assets_is_not_enabled(): void { - self::arrange(fn () => TestAssetFactory::simpleAsset()->save()); + self::arrange(fn () => TestAssetFactory::simpleAsset(5)->save()); - $this->client->request('GET', '/get-asset?id=42'); + $this->client->request('GET', '/get-asset?id=5'); $response = $this->client->getResponse(); self::assertSame('This is the content of the test asset.', $response->getContent()); @@ -78,10 +78,10 @@ public function response_is_not_tagged_when_assets_is_not_enabled(): void ])] public function response_is_not_tagged_when_caching_is_deactivated(): void { - self::arrange(fn () => TestAssetFactory::simpleAsset()->save()); + self::arrange(fn () => TestAssetFactory::simpleAsset(5)->save()); self::getContainer()->get(CacheActivator::class)->deactivateCaching(); - $this->client->request('GET', '/get-asset?id=42'); + $this->client->request('GET', '/get-asset?id=5'); $response = $this->client->getResponse(); self::assertSame('This is the content of the test asset.', $response->getContent()); @@ -101,9 +101,9 @@ public function response_is_not_tagged_when_caching_is_deactivated(): void ])] public function response_is_not_tagged_when_asset_is_of_type_folder(): void { - self::arrange(fn () => TestAssetFactory::simpleFolder()->save()); + self::arrange(fn () => TestAssetFactory::simpleFolder(12)->save()); - $this->client->request('GET', '/get-asset?id=23'); + $this->client->request('GET', '/get-asset?id=12'); $response = $this->client->getResponse(); self::assertSame('', $response->getContent()); @@ -128,9 +128,9 @@ public function response_is_not_tagged_when_asset_is_of_type_folder(): void ])] public function response_is_not_tagged_for_specified_asset_type(): void { - self::arrange(fn () => TestAssetFactory::simpleImage()->save()); + self::arrange(fn () => TestAssetFactory::simpleImage(29)->save()); - $this->client->request('GET', '/get-asset?id=17'); + $this->client->request('GET', '/get-asset?id=29'); $response = $this->client->getResponse(); self::assertSame('', $response->getContent()); diff --git a/tests/Integration/Tagging/TagDocumentTest.php b/tests/Integration/Tagging/TagDocumentTest.php index e6b1fd3..480f9d3 100644 --- a/tests/Integration/Tagging/TagDocumentTest.php +++ b/tests/Integration/Tagging/TagDocumentTest.php @@ -34,7 +34,7 @@ protected function setUp(): void ])] public function response_is_tagged_with_expected_tags_when_page_is_loaded(): void { - self::arrange(fn () => TestDocumentFactory::simplePage()->save()); + self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $this->client->request('GET', '/test_document_page'); @@ -43,7 +43,7 @@ public function response_is_tagged_with_expected_tags_when_page_is_loaded(): voi self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringContainsString('d42', $response->headers->get('X-Cache-Tags')); + self::assertStringContainsString('d5', $response->headers->get('X-Cache-Tags')); } /** @@ -56,16 +56,16 @@ public function response_is_tagged_with_expected_tags_when_page_is_loaded(): voi ])] public function response_is_tagged_with_expected_tags_when_snippet_is_loaded(): void { - self::arrange(fn () => TestDocumentFactory::simpleSnippet()->save()); + self::arrange(fn () => TestDocumentFactory::simpleSnippet(12)->save()); - $this->client->request('GET', '/get-document?id=23'); + $this->client->request('GET', '/get-document?id=12'); $response = $this->client->getResponse(); self::assertSame('Document with key: test_document_snippet', $response->getContent()); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringContainsString('d23', $response->headers->get('X-Cache-Tags')); + self::assertStringContainsString('d12', $response->headers->get('X-Cache-Tags')); } /** @@ -78,16 +78,16 @@ public function response_is_tagged_with_expected_tags_when_snippet_is_loaded(): ])] public function response_is_not_tagged_when_document_type_is_email(): void { - self::arrange(fn () => TestDocumentFactory::simpleEmail()->save()); + self::arrange(fn () => TestDocumentFactory::simpleEmail(29)->save()); - $this->client->request('GET', '/get-document?id=17'); + $this->client->request('GET', '/get-document?id=29'); $response = $this->client->getResponse(); self::assertSame('Document with key: test_document_link', $response->getContent()); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringNotContainsString('d17', $response->headers->get('X-Cache-Tags')); + self::assertStringNotContainsString('d29', $response->headers->get('X-Cache-Tags')); } /** @@ -100,16 +100,16 @@ public function response_is_not_tagged_when_document_type_is_email(): void ])] public function response_is_not_tagged_when_document_type_is_hard_link(): void { - self::arrange(fn () => TestDocumentFactory::simpleHardLink()->save()); + self::arrange(fn () => TestDocumentFactory::simpleHardLink(12)->save()); - $this->client->request('GET', '/get-document?id=33'); + $this->client->request('GET', '/get-document?id=12'); $response = $this->client->getResponse(); self::assertSame('Document with key: test_document_hard_link', $response->getContent()); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringNotContainsString('d33', $response->headers->get('X-Cache-Tags')); + self::assertStringNotContainsString('d29', $response->headers->get('X-Cache-Tags')); } /** @@ -122,16 +122,16 @@ public function response_is_not_tagged_when_document_type_is_hard_link(): void ])] public function response_is_not_tagged_when_document_type_is_folder(): void { - self::arrange(fn () => TestDocumentFactory::simpleFolder()->save()); + self::arrange(fn () => TestDocumentFactory::simpleFolder(70)->save()); - $this->client->request('GET', '/get-document?id=97'); + $this->client->request('GET', '/get-document?id=70'); $response = $this->client->getResponse(); self::assertSame('Document with key: test_document_folder', $response->getContent()); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringNotContainsString('d97', $response->headers->get('X-Cache-Tags')); + self::assertStringNotContainsString('d70', $response->headers->get('X-Cache-Tags')); } /** @@ -144,7 +144,7 @@ public function response_is_not_tagged_when_document_type_is_folder(): void ])] public function response_is_not_tagged_when_documents_is_not_enabled(): void { - self::arrange(fn () => TestDocumentFactory::simplePage()->save()); + self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $this->client->request('GET', '/test_document_page'); @@ -166,7 +166,7 @@ public function response_is_not_tagged_when_documents_is_not_enabled(): void ])] public function response_is_not_tagged_when_caching_is_deactivated(): void { - self::arrange(fn () => TestDocumentFactory::simplePage()->save()); + self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); self::getContainer()->get(CacheActivator::class)->deactivateCaching(); $this->client->request('GET', '/test_document_page'); @@ -189,7 +189,7 @@ public function response_is_not_tagged_when_caching_is_deactivated(): void ])] public function response_is_tagged_with_root_document_tag_when_loaded(): void { - self::arrange(fn () => TestDocumentFactory::simplePage()->save()); + self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $this->client->request('GET', '/test_document_page'); @@ -215,7 +215,7 @@ public function response_is_tagged_with_root_document_tag_when_loaded(): void ])] public function response_is_not_tagged_when_type_is_disabled(): void { - self::arrange(fn () => TestDocumentFactory::simplePage()->save()); + self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $this->client->request('GET', '/test_document_page'); diff --git a/tests/Integration/Tagging/TagObjectTest.php b/tests/Integration/Tagging/TagObjectTest.php index a502917..897b390 100644 --- a/tests/Integration/Tagging/TagObjectTest.php +++ b/tests/Integration/Tagging/TagObjectTest.php @@ -34,16 +34,16 @@ protected function setUp(): void ])] public function response_is_tagged_with_expected_tags_when_object_is_loaded(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $response = $this->client->getResponse(); self::assertSame('Test content', $response->getContent()); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertSame('o42', $response->headers->get('X-Cache-Tags')); + self::assertSame('o5', $response->headers->get('X-Cache-Tags')); } /** @@ -56,9 +56,9 @@ public function response_is_tagged_with_expected_tags_when_object_is_loaded(): v ])] public function response_is_not_tagged_when_objects_is_not_enabled(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $response = $this->client->getResponse(); self::assertSame('Test content', $response->getContent()); @@ -78,10 +78,10 @@ public function response_is_not_tagged_when_objects_is_not_enabled(): void ])] public function response_is_not_tagged_when_caching_is_deactivated(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); self::getContainer()->get(CacheActivator::class)->deactivateCaching(); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $response = $this->client->getResponse(); self::assertSame('Test content', $response->getContent()); @@ -106,9 +106,9 @@ public function response_is_not_tagged_when_caching_is_deactivated(): void ])] public function response_is_not_tagged_when_object_type_is_disabled(): void { - self::arrange(fn () => TestObjectFactory::simpleVariant()->save()); + self::arrange(fn () => TestObjectFactory::simpleVariant(5)->save()); - $this->client->request('GET', '/get-object?id=17'); + $this->client->request('GET', '/get-object?id=5'); $response = $this->client->getResponse(); self::assertSame('Test content', $response->getContent()); @@ -133,9 +133,9 @@ public function response_is_not_tagged_when_object_type_is_disabled(): void ])] public function response_ist_tagged_when_object_type_is_enabled(): void { - self::arrange(fn () => TestObjectFactory::simpleVariant()->save()); + self::arrange(fn () => TestObjectFactory::simpleVariant(12)->save()); - $this->client->request('GET', '/get-object?id=17'); + $this->client->request('GET', '/get-object?id=12'); $response = $this->client->getResponse(); self::assertSame('Test content', $response->getContent()); @@ -160,9 +160,9 @@ public function response_ist_tagged_when_object_type_is_enabled(): void ])] public function response_is_not_tagged_when_object_class_is_disabled(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $response = $this->client->getResponse(); self::assertSame('Test content', $response->getContent()); @@ -187,15 +187,15 @@ public function response_is_not_tagged_when_object_class_is_disabled(): void ])] public function response_is_tagged_when_object_class_is_enabled(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $response = $this->client->getResponse(); self::assertSame('Test content', $response->getContent()); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertSame('o42', $response->headers->get('X-Cache-Tags')); + self::assertSame('o5', $response->headers->get('X-Cache-Tags')); } } From c0ede1bb146b031ed888ab2c1ca0fda1734be321 Mon Sep 17 00:00:00 2001 From: jadams Date: Wed, 13 Aug 2025 12:47:10 +0200 Subject: [PATCH 03/40] Add testcases for asset & document invalidation --- src/Element/InvalidateElementListener.php | 3 +- .../Integration/Helpers/TestObjectFactory.php | 9 +- .../Invalidation/InvalidateObjectTest.php | 147 ++++++++++++++-- .../classes/definition_TestDataObject.php | 164 ------------------ .../pimcore/classes/definition_TestObject.php | 26 ++- 5 files changed, 151 insertions(+), 198 deletions(-) delete mode 100644 tests/app/config/pimcore/classes/definition_TestDataObject.php diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index a7a21f7..70768e3 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -52,8 +52,7 @@ private function invalidateElement(ElementInterface $element): void private function invalidateDependencies(Dependency $dependency): void { - $requiredBy = $dependency->getRequiredBy(); - foreach ($requiredBy as $required) { + foreach ($dependency->getRequiredBy() as $required) { if (!isset($required['id'], $required['type'])) { continue; } diff --git a/tests/Integration/Helpers/TestObjectFactory.php b/tests/Integration/Helpers/TestObjectFactory.php index 70aec12..7e317c4 100644 --- a/tests/Integration/Helpers/TestObjectFactory.php +++ b/tests/Integration/Helpers/TestObjectFactory.php @@ -2,15 +2,16 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers; +use Pimcore\Image; use Pimcore\Model\DataObject; use Pimcore\Model\DataObject\AbstractObject; -use Pimcore\Model\DataObject\TestDataObject; use Pimcore\Model\DataObject\TestObject; +use Pimcore\Model\Document\Page; final class TestObjectFactory { /** - * @param list $related + * @param list $related */ public static function simpleObject(int $id, string $key = 'test_object', array $related = []): TestObject { @@ -25,9 +26,9 @@ public static function simpleObject(int $id, string $key = 'test_object', array return $object; } - public static function simpleVariant(int $id, string $key = 'simple_variant'): TestDataObject + public static function simpleVariant(int $id, string $key = 'simple_variant'): TestObject { - $object = new TestDataObject(); + $object = new TestObject(); $object->setId($id); $object->setKey($key); $object->setContent('Test content'); diff --git a/tests/Integration/Invalidation/InvalidateObjectTest.php b/tests/Integration/Invalidation/InvalidateObjectTest.php index b5e6c70..ecae836 100644 --- a/tests/Integration/Invalidation/InvalidateObjectTest.php +++ b/tests/Integration/Invalidation/InvalidateObjectTest.php @@ -3,13 +3,15 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Invalidation; use FOS\HttpCacheBundle\CacheManager; +use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\ArrangeCacheTest; +use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestAssetFactory; +use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestDocumentFactory; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestObjectFactory; use Neusta\Pimcore\TestingFramework\Database\ResetDatabase; use Neusta\Pimcore\TestingFramework\Test\Attribute\ConfigureExtension; use Neusta\Pimcore\TestingFramework\Test\ConfigurableKernelTestCase; use Pimcore\Model\DataObject; -use Pimcore\Model\DataObject\TestDataObject; use Pimcore\Model\DataObject\TestObject; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -26,9 +28,7 @@ final class InvalidateObjectTest extends ConfigurableKernelTestCase private TestObject $object; - private TestObject $otherObject; - - private TestDataObject $variant; + private TestObject $variant; private DataObject\Folder $folder; @@ -37,8 +37,8 @@ protected function setUp(): void $this->cacheManager = $this->prophesize(CacheManager::class); $this->cacheManager->invalidateTags(Argument::any())->willReturn($this->cacheManager->reveal()); self::getContainer()->set('fos_http_cache.cache_manager', $this->cacheManager->reveal()); + $this->object = self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); - $this->otherObject = self::arrange(fn () => TestObjectFactory::simpleObject(12, 'other_test_object', [$this->object])->save()); $this->folder = self::arrange(fn () => TestObjectFactory::simpleFolder(29)->save()); $this->variant = self::arrange(fn () => TestObjectFactory::simpleVariant(70)->save()); } @@ -55,7 +55,8 @@ public function response_is_invalidated_when_object_is_updated(): void { $this->object->setContent('Updated test content')->save(); - $this->cacheManager->invalidateTags(['o5'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags([CacheTag::fromElement($this->object)->toString()]) + ->shouldHaveBeenCalledTimes(1); } /** @@ -63,20 +64,63 @@ public function response_is_invalidated_when_object_is_updated(): void */ #[ConfigureExtension('neusta_pimcore_http_cache', [ 'elements' => [ - 'objects' => [ - 'enabled' => true, - 'types' => [ - 'variant' => true, - ], - ], + 'objects' => true, ], ])] - public function dependent_element_is_invalidated_on_update(): void + public function dependent_object_is_invalidated_on_object_update(): void { - $this->otherObject->setContent('Updated content')->save(); + $dependent = self::arrange( + fn () => TestObjectFactory::simpleObject(12, 'other_test_object', [$this->object])->save(), + ); + + $this->object->setContent('Updated test content')->save(); - $this->cacheManager->invalidateTags(['o5'])->shouldHaveBeenCalledTimes(1); - $this->cacheManager->invalidateTags(['o12'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags([CacheTag::fromElement($dependent)->toString()]) + ->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => true, + 'assets' => true, + ], + ])] + public function dependent_object_is_invalidated_on_asset_update(): void + { + $asset = self::arrange(fn () => TestAssetFactory::simpleImage(99)->save()); + $dependent = self::arrange( + fn () => TestObjectFactory::simpleObject(12, 'other_test_object', [$asset])->save(), + ); + + $asset->setData('Updated test content')->save(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($dependent)->toString()]) + ->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => true, + 'documents' => true, + ], + ])] + public function dependent_object_is_invalidated_on_document_update(): void + { + $document = self::arrange(fn () => TestDocumentFactory::simplePage(99)->save()); + $dependent = self::arrange( + fn () => TestObjectFactory::simpleObject(12, 'other_test_object', [$document])->save(), + ); + + $document->setKey('updated_test_document')->save(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($dependent)->toString()]) + ->shouldHaveBeenCalledTimes(1); } /** @@ -91,7 +135,72 @@ public function response_is_invalidated_when_object_is_deleted(): void { $this->object->delete(); - $this->cacheManager->invalidateTags(['o5'])->shouldHaveBeenCalledTimes(1); + $this->cacheManager->invalidateTags([CacheTag::fromElement($this->object)->toString()]) + ->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => true, + ], + ])] + public function dependent_object_is_invalidated_on_object_deletion(): void + { + $dependent = self::arrange( + fn () => TestObjectFactory::simpleObject(12, 'other_test_object', [$this->object])->save(), + ); + + $this->object->delete(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($dependent)->toString()]) + ->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => true, + 'assets' => true, + ], + ])] + public function dependent_object_is_invalidated_on_asset_deletion(): void + { + $asset = self::arrange(fn () => TestAssetFactory::simpleImage(99)->save()); + $dependent = self::arrange( + fn () => TestObjectFactory::simpleObject(12, 'other_test_object', [$asset])->save(), + ); + + $asset->delete(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($dependent)->toString()]) + ->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => true, + 'documents' => true, + ], + ])] + public function dependent_object_is_invalidated_on_document_deletion(): void + { + $document = self::arrange(fn () => TestDocumentFactory::simplePage(99)->save()); + $dependent = self::arrange( + fn () => TestObjectFactory::simpleObject(12, 'other_test_object', [$document])->save(), + ); + + $document->delete(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($dependent)->toString()]) + ->shouldHaveBeenCalledTimes(1); } /** @@ -172,7 +281,7 @@ public function response_is_not_invalidated_when_specified_type_is_disabled_on_d 'objects' => [ 'enabled' => true, 'classes' => [ - 'TestDataObject' => false, + 'TestObject' => false, ], ], ], @@ -192,7 +301,7 @@ public function response_is_not_invalidated_when_custom_data_object_class_is_dis 'objects' => [ 'enabled' => true, 'classes' => [ - 'TestDataObject' => false, + 'TestObject' => false, ], ], ], diff --git a/tests/app/config/pimcore/classes/definition_TestDataObject.php b/tests/app/config/pimcore/classes/definition_TestDataObject.php deleted file mode 100644 index b789edd..0000000 --- a/tests/app/config/pimcore/classes/definition_TestDataObject.php +++ /dev/null @@ -1,164 +0,0 @@ - null, - 'id' => 'test_data_object', - 'name' => 'TestDataObject', - 'description' => '', - 'creationDate' => 0, - 'modificationDate' => 1685448671, - 'userOwner' => 1, - 'userModification' => 1, - 'parentClass' => '', - 'implementsInterfaces' => '', - 'listingParentClass' => '', - 'useTraits' => '', - 'listingUseTraits' => '', - 'encryption' => false, - 'encryptedTables' => [ - ], - 'allowInherit' => false, - 'allowVariants' => false, - 'showVariants' => false, - 'fieldDefinitions' => [ - ], - 'layoutDefinitions' => Pimcore\Model\DataObject\ClassDefinition\Layout\Panel::__set_state([ - 'name' => 'pimcore_root', - 'type' => null, - 'region' => null, - 'title' => null, - 'width' => 0, - 'height' => 0, - 'collapsible' => false, - 'collapsed' => false, - 'bodyStyle' => null, - 'datatype' => 'layout', - 'permissions' => null, - 'children' => [ - 0 => Pimcore\Model\DataObject\ClassDefinition\Layout\Panel::__set_state([ - 'name' => 'Layout', - 'type' => null, - 'region' => null, - 'title' => '', - 'width' => null, - 'height' => null, - 'collapsible' => false, - 'collapsed' => false, - 'bodyStyle' => '', - 'datatype' => 'layout', - 'permissions' => null, - 'children' => [ - 0 => Pimcore\Model\DataObject\ClassDefinition\Data\Input::__set_state([ - 'name' => 'content', - 'title' => 'Content', - 'tooltip' => '', - 'mandatory' => true, - 'noteditable' => false, - 'index' => false, - 'locked' => false, - 'style' => '', - 'permissions' => null, - 'datatype' => 'data', - 'fieldtype' => 'input', - 'relationType' => false, - 'invisible' => false, - 'visibleGridView' => false, - 'visibleSearch' => false, - 'blockedVarsForExport' => [ - ], - 'width' => null, - 'defaultValue' => null, - 'columnLength' => 190, - 'regex' => '', - 'regexFlags' => [ - ], - 'unique' => false, - 'showCharCount' => false, - 'defaultValueGenerator' => '', - ]), - 1 => Pimcore\Model\DataObject\ClassDefinition\Data\ManyToOneRelation::__set_state([ - 'name' => 'relatedObjects', - 'title' => 'Related Objects', - 'tooltip' => '', - 'mandatory' => false, - 'noteditable' => false, - 'index' => false, - 'locked' => false, - 'style' => '', - 'permissions' => null, - 'datatype' => 'data', - 'invisible' => false, - 'visibleGridView' => false, - 'visibleSearch' => false, - 'classes' => [ - 0 => [ - 'classes' => 'TestDataObject', - ], - ], - 'pathFormatterClass' => '', - 'width' => '', - 'assetUploadPath' => '', - 'objectsAllowed' => true, - 'assetsAllowed' => false, - 'assetTypes' => [], - 'documentsAllowed' => false, - 'documentTypes' => [], - ]), - ], - 'locked' => false, - 'blockedVarsForExport' => [ - ], - 'fieldtype' => 'panel', - 'layout' => null, - 'border' => false, - 'icon' => null, - 'labelWidth' => 100, - 'labelAlign' => 'left', - ]), - ], - 'locked' => false, - 'blockedVarsForExport' => [ - ], - 'fieldtype' => 'panel', - 'layout' => null, - 'border' => false, - 'icon' => null, - 'labelWidth' => 100, - 'labelAlign' => 'left', - ]), - 'icon' => '', - 'previewUrl' => '', - 'group' => '', - 'showAppLoggerTab' => false, - 'linkGeneratorReference' => '', - 'previewGeneratorReference' => '', - 'compositeIndices' => [ - ], - 'generateTypeDeclarations' => true, - 'showFieldLookup' => false, - 'propertyVisibility' => [ - 'grid' => [ - 'id' => true, - 'key' => false, - 'path' => true, - 'published' => true, - 'modificationDate' => true, - 'creationDate' => true, - ], - 'search' => [ - 'id' => true, - 'key' => false, - 'path' => true, - 'published' => true, - 'modificationDate' => true, - 'creationDate' => true, - ], - ], - 'enableGridLocking' => false, - 'deletedDataComponents' => [ - ], - 'blockedVarsForExport' => [ - ], - 'activeDispatchingEvents' => [ - ], -]); diff --git a/tests/app/config/pimcore/classes/definition_TestObject.php b/tests/app/config/pimcore/classes/definition_TestObject.php index 294d396..f556bc9 100644 --- a/tests/app/config/pimcore/classes/definition_TestObject.php +++ b/tests/app/config/pimcore/classes/definition_TestObject.php @@ -6,7 +6,7 @@ * * Fields Summary: * - content [input] - * - related [manyToManyObjectRelation] + * - related [manyToManyRelation] */ return Pimcore\Model\DataObject\ClassDefinition::__set_state([ @@ -15,7 +15,7 @@ 'name' => 'TestObject', 'description' => '', 'creationDate' => 0, - 'modificationDate' => 1754659392, + 'modificationDate' => 1755079708, 'userOwner' => 58, 'userModification' => 58, 'parentClass' => '', @@ -85,7 +85,7 @@ 'showCharCount' => false, 'defaultValueGenerator' => '', ]), - 1 => Pimcore\Model\DataObject\ClassDefinition\Data\ManyToManyObjectRelation::__set_state([ + 1 => Pimcore\Model\DataObject\ClassDefinition\Data\ManyToManyRelation::__set_state([ 'name' => 'related', 'title' => 'Related', 'tooltip' => '', @@ -96,7 +96,7 @@ 'style' => '', 'permissions' => null, 'datatype' => 'data', - 'fieldtype' => 'manyToManyObjectRelation', + 'fieldtype' => 'manyToManyRelation', 'relationType' => true, 'invisible' => false, 'visibleGridView' => false, @@ -112,13 +112,21 @@ 'width' => '', 'height' => '', 'maxItems' => null, - 'visibleFields' => [ + 'assetUploadPath' => '', + 'objectsAllowed' => true, + 'assetsAllowed' => true, + 'assetTypes' => [ + 0 => [ + 'assetTypes' => 'image', + ], ], - 'allowToCreateNewObject' => false, - 'optimizedAdminLoading' => false, - 'enableTextSelection' => false, - 'visibleFieldDefinitions' => [ + 'documentsAllowed' => true, + 'documentTypes' => [ + 0 => [ + 'documentTypes' => 'page', + ], ], + 'enableTextSelection' => false, ]), ], 'locked' => false, From b3161a456a679aa835d8122de7d62bc27d15d8bf Mon Sep 17 00:00:00 2001 From: jadams Date: Wed, 20 Aug 2025 12:16:18 +0200 Subject: [PATCH 04/40] Only invalidate dependencies from objects --- src/Element/InvalidateElementListener.php | 9 +- .../Invalidation/InvalidateObjectTest.php | 90 ------------------- .../Tagging/CollectTagsDataTest.php | 46 +++++----- tests/Integration/Tagging/TagDocumentTest.php | 2 +- tests/Integration/Tagging/TagObjectTest.php | 6 +- .../Element/InvalidateElementListenerTest.php | 47 +++++++++- .../src/Controller/GetObjectController.php | 4 +- 7 files changed, 80 insertions(+), 124 deletions(-) diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index 70768e3..ec08bd7 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -27,7 +27,9 @@ public function onUpdate(ElementEventInterface $event): void $this->invalidateElement($element); - $this->invalidateDependencies($element->getDependencies()); + if (ElementType::Object === ElementType::tryFrom($element->getType())) { + $this->invalidateDependencies($element->getDependencies()); + } } public function onDelete(ElementEventInterface $event): void @@ -35,7 +37,10 @@ public function onDelete(ElementEventInterface $event): void $element = $event->getElement(); $this->invalidateElement($element); - $this->invalidateDependencies($element->getDependencies()); + + if (ElementType::Object === ElementType::tryFrom($element->getType())) { + $this->invalidateDependencies($element->getDependencies()); + } } private function invalidateElement(ElementInterface $element): void diff --git a/tests/Integration/Invalidation/InvalidateObjectTest.php b/tests/Integration/Invalidation/InvalidateObjectTest.php index ecae836..182233d 100644 --- a/tests/Integration/Invalidation/InvalidateObjectTest.php +++ b/tests/Integration/Invalidation/InvalidateObjectTest.php @@ -5,8 +5,6 @@ use FOS\HttpCacheBundle\CacheManager; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\ArrangeCacheTest; -use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestAssetFactory; -use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestDocumentFactory; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestObjectFactory; use Neusta\Pimcore\TestingFramework\Database\ResetDatabase; use Neusta\Pimcore\TestingFramework\Test\Attribute\ConfigureExtension; @@ -79,50 +77,6 @@ public function dependent_object_is_invalidated_on_object_update(): void ->shouldHaveBeenCalledTimes(1); } - /** - * @test - */ - #[ConfigureExtension('neusta_pimcore_http_cache', [ - 'elements' => [ - 'objects' => true, - 'assets' => true, - ], - ])] - public function dependent_object_is_invalidated_on_asset_update(): void - { - $asset = self::arrange(fn () => TestAssetFactory::simpleImage(99)->save()); - $dependent = self::arrange( - fn () => TestObjectFactory::simpleObject(12, 'other_test_object', [$asset])->save(), - ); - - $asset->setData('Updated test content')->save(); - - $this->cacheManager->invalidateTags([CacheTag::fromElement($dependent)->toString()]) - ->shouldHaveBeenCalledTimes(1); - } - - /** - * @test - */ - #[ConfigureExtension('neusta_pimcore_http_cache', [ - 'elements' => [ - 'objects' => true, - 'documents' => true, - ], - ])] - public function dependent_object_is_invalidated_on_document_update(): void - { - $document = self::arrange(fn () => TestDocumentFactory::simplePage(99)->save()); - $dependent = self::arrange( - fn () => TestObjectFactory::simpleObject(12, 'other_test_object', [$document])->save(), - ); - - $document->setKey('updated_test_document')->save(); - - $this->cacheManager->invalidateTags([CacheTag::fromElement($dependent)->toString()]) - ->shouldHaveBeenCalledTimes(1); - } - /** * @test */ @@ -159,50 +113,6 @@ public function dependent_object_is_invalidated_on_object_deletion(): void ->shouldHaveBeenCalledTimes(1); } - /** - * @test - */ - #[ConfigureExtension('neusta_pimcore_http_cache', [ - 'elements' => [ - 'objects' => true, - 'assets' => true, - ], - ])] - public function dependent_object_is_invalidated_on_asset_deletion(): void - { - $asset = self::arrange(fn () => TestAssetFactory::simpleImage(99)->save()); - $dependent = self::arrange( - fn () => TestObjectFactory::simpleObject(12, 'other_test_object', [$asset])->save(), - ); - - $asset->delete(); - - $this->cacheManager->invalidateTags([CacheTag::fromElement($dependent)->toString()]) - ->shouldHaveBeenCalledTimes(1); - } - - /** - * @test - */ - #[ConfigureExtension('neusta_pimcore_http_cache', [ - 'elements' => [ - 'objects' => true, - 'documents' => true, - ], - ])] - public function dependent_object_is_invalidated_on_document_deletion(): void - { - $document = self::arrange(fn () => TestDocumentFactory::simplePage(99)->save()); - $dependent = self::arrange( - fn () => TestObjectFactory::simpleObject(12, 'other_test_object', [$document])->save(), - ); - - $document->delete(); - - $this->cacheManager->invalidateTags([CacheTag::fromElement($dependent)->toString()]) - ->shouldHaveBeenCalledTimes(1); - } - /** * @test */ diff --git a/tests/Integration/Tagging/CollectTagsDataTest.php b/tests/Integration/Tagging/CollectTagsDataTest.php index 6a83a95..de3cab4 100644 --- a/tests/Integration/Tagging/CollectTagsDataTest.php +++ b/tests/Integration/Tagging/CollectTagsDataTest.php @@ -46,7 +46,7 @@ protected function setUp(): void #[ConfigureRoute(__DIR__ . '/../Fixtures/get_document_route.php')] public function collect_tags_for_type_document(): void { - self::arrange(fn () => TestDocumentFactory::simplePage())->save(); + self::arrange(fn () => TestDocumentFactory::simplePage(5))->save(); $this->client->request('GET', '/test_document_page'); $this->client->enableProfiler(); @@ -55,7 +55,7 @@ public function collect_tags_for_type_document(): void self::assertInstanceOf(DataCollector::class, $dataCollector); self::assertSame( - [['tag' => 'd1', 'type' => 'document'], ['tag' => 'd42', 'type' => 'document']], + [['tag' => 'd1', 'type' => 'document'], ['tag' => 'd5', 'type' => 'document']], $dataCollector->getTags(), ); } @@ -71,16 +71,16 @@ public function collect_tags_for_type_document(): void #[ConfigureRoute(__DIR__ . '/../Fixtures/get_object_route.php')] public function collect_tags_for_type_object(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $this->client->enableProfiler(); $dataCollector = $this->client->getProfile()->getCollector('pimcore_http_cache'); self::assertInstanceOf(DataCollector::class, $dataCollector); self::assertSame( - [['tag' => 'o42', 'type' => 'object']], + [['tag' => 'o5', 'type' => 'object']], $dataCollector->getTags(), ); } @@ -96,16 +96,16 @@ public function collect_tags_for_type_object(): void #[ConfigureRoute(__DIR__ . '/../Fixtures/get_asset_route.php')] public function collect_tags_of_type_asset(): void { - self::arrange(fn () => TestAssetFactory::simpleAsset()->save()); + self::arrange(fn () => TestAssetFactory::simpleAsset(5)->save()); - $this->client->request('GET', '/get-asset?id=42'); + $this->client->request('GET', '/get-asset?id=5'); $this->client->enableProfiler(); $dataCollector = $this->client->getProfile()->getCollector('pimcore_http_cache'); self::assertInstanceOf(DataCollector::class, $dataCollector); self::assertSame( - [['tag' => 'a42', 'type' => 'asset']], + [['tag' => 'a5', 'type' => 'asset']], $dataCollector->getTags(), ); } @@ -124,7 +124,7 @@ public function collect_tags_of_type_asset(): void #[ConfigureRoute(__DIR__ . '/../Fixtures/get_object_route.php')] public function collect_tags_of_type_custom(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); self::getContainer()->get('event_dispatcher')->addListener( ElementTaggingEvent::class, @@ -133,7 +133,7 @@ public function collect_tags_of_type_custom(): void ), ); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $this->client->enableProfiler(); $dataCollector = $this->client->getProfile()->getCollector('pimcore_http_cache'); @@ -156,9 +156,9 @@ public function collect_tags_of_type_custom(): void #[ConfigureRoute(__DIR__ . '/../Fixtures/get_object_route.php')] public function does_not_collect_tags_when_type_is_disabled(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $this->client->enableProfiler(); $dataCollector = $this->client->getProfile()->getCollector('pimcore_http_cache'); @@ -178,10 +178,10 @@ public function does_not_collect_tags_when_type_is_disabled(): void #[ConfigureRoute(__DIR__ . '/../Fixtures/get_object_route.php')] public function does_not_collect_tags_when_caching_is_disabled(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); self::getContainer()->get(CacheActivator::class)->deactivateCaching(); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $this->client->enableProfiler(); $dataCollector = $this->client->getProfile()->getCollector('pimcore_http_cache'); @@ -206,9 +206,9 @@ public function does_not_collect_tags_when_caching_is_disabled(): void #[ConfigureRoute(__DIR__ . '/../Fixtures/get_object_route.php')] public function does_not_collect_tags_when_object_type_is_disabled(): void { - self::arrange(fn () => TestObjectFactory::simpleVariant()->save()); + self::arrange(fn () => TestObjectFactory::simpleVariant(5)->save()); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $this->client->enableProfiler(); $dataCollector = $this->client->getProfile()->getCollector('pimcore_http_cache'); @@ -224,7 +224,7 @@ public function does_not_collect_tags_when_object_type_is_disabled(): void 'elements' => [ 'objects' => [ 'classes' => [ - 'TestDataObject' => false, + 'TestObject' => false, ], 'enabled' => true, ], @@ -233,9 +233,9 @@ public function does_not_collect_tags_when_object_type_is_disabled(): void #[ConfigureRoute(__DIR__ . '/../Fixtures/get_object_route.php')] public function does_not_collect_tags_when_object_class_is_disabled(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $this->client->enableProfiler(); $dataCollector = $this->client->getProfile()->getCollector('pimcore_http_cache'); @@ -261,9 +261,9 @@ public function does_not_collect_tags_when_object_class_is_disabled(): void #[ConfigureRoute(__DIR__ . '/../Fixtures/get_object_route.php')] public function does_not_collect_tags_when_profiler_is_disabled(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $this->client->enableProfiler(); self::assertFalse($this->client->getProfile()); @@ -278,9 +278,9 @@ public function does_not_collect_tags_when_profiler_is_disabled(): void #[ConfigureRoute(__DIR__ . '/../Fixtures/get_object_route.php')] public function does_not_collect_tags_when_collect_is_disabled(): void { - self::arrange(fn () => TestObjectFactory::simpleObject()->save()); + self::arrange(fn () => TestObjectFactory::simpleObject(5)->save()); - $this->client->request('GET', '/get-object?id=42'); + $this->client->request('GET', '/get-object?id=5'); $this->client->enableProfiler(); $dataCollector = $this->client->getProfile()->getCollector('pimcore_http_cache'); diff --git a/tests/Integration/Tagging/TagDocumentTest.php b/tests/Integration/Tagging/TagDocumentTest.php index 480f9d3..66efe47 100644 --- a/tests/Integration/Tagging/TagDocumentTest.php +++ b/tests/Integration/Tagging/TagDocumentTest.php @@ -83,7 +83,7 @@ public function response_is_not_tagged_when_document_type_is_email(): void $this->client->request('GET', '/get-document?id=29'); $response = $this->client->getResponse(); - self::assertSame('Document with key: test_document_link', $response->getContent()); + self::assertSame('Document with key: test_document_email', $response->getContent()); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); diff --git a/tests/Integration/Tagging/TagObjectTest.php b/tests/Integration/Tagging/TagObjectTest.php index 897b390..7386464 100644 --- a/tests/Integration/Tagging/TagObjectTest.php +++ b/tests/Integration/Tagging/TagObjectTest.php @@ -142,7 +142,7 @@ public function response_ist_tagged_when_object_type_is_enabled(): void self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertSame('o17', $response->headers->get('X-Cache-Tags')); + self::assertSame('o12', $response->headers->get('X-Cache-Tags')); } /** @@ -152,7 +152,7 @@ public function response_ist_tagged_when_object_type_is_enabled(): void 'elements' => [ 'objects' => [ 'classes' => [ - 'TestDataObject' => false, + 'TestObject' => false, ], 'enabled' => true, ], @@ -179,7 +179,7 @@ public function response_is_not_tagged_when_object_class_is_disabled(): void 'elements' => [ 'objects' => [ 'classes' => [ - 'TestDataObject' => true, + 'TestObject' => true, ], 'enabled' => true, ], diff --git a/tests/Unit/Element/InvalidateElementListenerTest.php b/tests/Unit/Element/InvalidateElementListenerTest.php index eeed689..5acf14c 100644 --- a/tests/Unit/Element/InvalidateElementListenerTest.php +++ b/tests/Unit/Element/InvalidateElementListenerTest.php @@ -7,6 +7,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTags; use Neusta\Pimcore\HttpCacheBundle\Element\ElementInvalidationEvent; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener; use PHPUnit\Framework\TestCase; use Pimcore\Event\Model\AssetEvent; @@ -115,9 +116,10 @@ public function onUpdate_should_invalidate_elements(ElementEventInterface $event /** * @test */ - public function onUpdate_should_invalidate_dependent_elements(): void + public function onUpdate_should_invalidate_dependencies(): void { - $element = $this->prophesize(DataObject\TestDataObject::class); + $element = $this->prophesize(DataObject\TestObject::class); + $element->getType()->willReturn(ElementType::Object->value); $dependency = $this->prophesize(Dependency::class); $dependentElement = $this->prophesize(DataObject::class); $event = new DataObjectEvent($element->reveal()); @@ -134,6 +136,41 @@ public function onUpdate_should_invalidate_dependent_elements(): void ->shouldHaveBeenCalledOnce(); } + /** + * @test + * + * @dataProvider notObjectElementProvider + */ + public function onUpdate_should_not_invalidate_dependencies_when_element_is_not_an_object( + ElementEventInterface $event, + ): void { + $this->invalidateElementListener->onUpdate($event); + + $this->cacheInvalidator->invalidate(Argument::any()) + ->shouldHaveBeenCalledOnce(); + $this->elementRepository->findObject(Argument::any()) + ->shouldNotHaveBeenCalled(); + } + + public function notObjectElementProvider(): iterable + { + $asset = $this->prophesize(Asset::class); + $dependency = $this->prophesize(Dependency::class); + $asset->getId()->willReturn(42); + $asset->getType()->willReturn(ElementType::Asset->value); + $asset->getDependencies()->willReturn($dependency->reveal()); + $dependency->getRequiredBy()->willReturn(['id' => 23, 'type' => 'object']); + yield 'Asset' => ['event' => new AssetEvent($asset->reveal())]; + + $document = $this->prophesize(Document::class); + $dependency = $this->prophesize(Dependency::class); + $document->getId()->willReturn(42); + $document->getType()->willReturn(ElementType::Document->value); + $document->getDependencies()->willReturn($dependency->reveal()); + $dependency->getRequiredBy()->willReturn(['id' => 23, 'type' => 'object']); + yield 'Document' => ['event' => new DocumentEvent($document->reveal())]; + } + /** * @test * @@ -211,7 +248,8 @@ public function onDelete_should_invalidate_elements(ElementEventInterface $event */ public function onDelete_should_invalidate_dependent_elements(): void { - $element = $this->prophesize(DataObject\TestDataObject::class); + $element = $this->prophesize(DataObject\TestObject::class); + $element->getType()->willReturn(ElementType::Object->value); $dependency = $this->prophesize(Dependency::class); $dependentElement = $this->prophesize(DataObject::class); $event = new DataObjectEvent($element->reveal()); @@ -279,17 +317,20 @@ public function elementProvider(): iterable $asset = $this->prophesize(Asset::class); $asset->getId()->willReturn(42); $asset->getDependencies()->willReturn($dependency->reveal()); + $asset->getType()->willReturn(ElementType::Asset->value); yield 'Asset' => ['event' => new AssetEvent($asset->reveal())]; $document = $this->prophesize(Document::class); $document->getId()->willReturn(42); $document->getDependencies()->willReturn($dependency->reveal()); + $document->getType()->willReturn(ElementType::Document->value); yield 'Document' => ['event' => new DocumentEvent($document->reveal())]; $dataObject = $this->prophesize(DataObject::class); $dataObject->getId()->willReturn(42); $dataObject->getDependencies()->willReturn($dependency->reveal()); $dependency->getRequiredBy()->willReturn([]); + $dataObject->getType()->willReturn(ElementType::Object->value); yield 'Object' => ['event' => new DataObjectEvent($dataObject->reveal())]; } } diff --git a/tests/app/src/Controller/GetObjectController.php b/tests/app/src/Controller/GetObjectController.php index fd69901..31443a1 100644 --- a/tests/app/src/Controller/GetObjectController.php +++ b/tests/app/src/Controller/GetObjectController.php @@ -2,7 +2,7 @@ namespace App\Controller; -use Pimcore\Model\DataObject\TestDataObject; +use Pimcore\Model\DataObject\TestObject; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -10,7 +10,7 @@ final class GetObjectController { public function __invoke(Request $request): Response { - if (!$object = TestDataObject::getById($request->query->get('id'))) { + if (!$object = TestObject::getById($request->query->get('id'))) { return new Response('Object not found', Response::HTTP_NOT_FOUND); } From 26561517d99750ee7b3de5f0681d11d4f89f21d2 Mon Sep 17 00:00:00 2001 From: jadams Date: Wed, 20 Aug 2025 15:54:04 +0200 Subject: [PATCH 05/40] Test invalidation for dependent documents --- .../Helpers/TestDocumentFactory.php | 16 +++++- .../Invalidation/InvalidateDocumentTest.php | 50 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/tests/Integration/Helpers/TestDocumentFactory.php b/tests/Integration/Helpers/TestDocumentFactory.php index 26c1398..489484e 100644 --- a/tests/Integration/Helpers/TestDocumentFactory.php +++ b/tests/Integration/Helpers/TestDocumentFactory.php @@ -2,6 +2,8 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers; +use Pimcore\Model\DataObject\TestObject; +use Pimcore\Model\Document\Editable\Relation; use Pimcore\Model\Document\Email; use Pimcore\Model\Document\Folder; use Pimcore\Model\Document\Hardlink; @@ -10,7 +12,7 @@ final class TestDocumentFactory { - public static function simplePage(int $id, string $key = 'test_document_page'): Page + public static function simplePage(int $id, string $key = 'test_document_page', ?TestObject $relatedObject = null): Page { $page = new Page(); $page->setId($id); @@ -18,6 +20,18 @@ public static function simplePage(int $id, string $key = 'test_document_page'): $page->setPublished(true); $page->setParentId(1); + if (null !== $relatedObject) { + $objectRelation = new Relation(); + $objectRelation->setName('relatedObject'); + $objectRelation->setDataFromResource([ + 'id' => $relatedObject->getId(), + 'type' => 'object', + 'subtype' => 'object', + ]); + + $page->setEditable($objectRelation); + } + return $page; } diff --git a/tests/Integration/Invalidation/InvalidateDocumentTest.php b/tests/Integration/Invalidation/InvalidateDocumentTest.php index 5837cc3..a421e91 100644 --- a/tests/Integration/Invalidation/InvalidateDocumentTest.php +++ b/tests/Integration/Invalidation/InvalidateDocumentTest.php @@ -3,8 +3,10 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Invalidation; use FOS\HttpCacheBundle\CacheManager; +use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\ArrangeCacheTest; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestDocumentFactory; +use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestObjectFactory; use Neusta\Pimcore\TestingFramework\Database\ResetDatabase; use Neusta\Pimcore\TestingFramework\Test\Attribute\ConfigureExtension; use Neusta\Pimcore\TestingFramework\Test\ConfigurableKernelTestCase; @@ -57,6 +59,30 @@ public function response_is_invalidated_when_document_is_updated(): void $this->cacheManager->invalidateTags(['d5'])->shouldHaveBeenCalledTimes(1); } + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => true, + 'documents' => true, + ], + ])] + public function dependent_document_is_invalidated_on_object_update(): void + { + $dependent = self::arrange( + fn () => TestObjectFactory::simpleObject(12)->save(), + ); + $document = self::arrange( + fn () => TestDocumentFactory::simplePage(96, 'other_test_document_page', $dependent)->save(), + ); + + $dependent->setContent('Updated test content')->save(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($document)->toString()]) + ->shouldHaveBeenCalledTimes(1); + } + /** * @test */ @@ -72,6 +98,30 @@ public function response_is_invalidated_when_document_is_deleted(): void $this->cacheManager->invalidateTags(['d5'])->shouldHaveBeenCalledTimes(1); } + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => true, + 'documents' => true, + ], + ])] + public function dependent_document_is_invalidated_on_object_deletion(): void + { + $dependent = self::arrange( + fn () => TestObjectFactory::simpleObject(12)->save(), + ); + $document = self::arrange( + fn () => TestDocumentFactory::simplePage(96, 'other_test_document_page', $dependent)->save(), + ); + + $dependent->delete(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($document)->toString()]) + ->shouldHaveBeenCalledTimes(1); + } + /** * @test */ From 365cd1af27f08adb62ea900b7cf971988a110a61 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 07:25:19 +0100 Subject: [PATCH 06/40] Fix review findings: final class, and negative dependency traversal tests - Make ElementRepository a final class (drop redundant @final annotation) - Add test: dependency traversal is not triggered when a document is updated - Add test: dependency traversal is not triggered when an asset is updated Co-Authored-By: Claude Sonnet 4.6 --- src/Element/ElementRepository.php | 8 ++----- .../Invalidation/InvalidateAssetTest.php | 23 ++++++++++++++++++ .../Invalidation/InvalidateDocumentTest.php | 24 +++++++++++++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/Element/ElementRepository.php b/src/Element/ElementRepository.php index 9822b0d..65ea35b 100644 --- a/src/Element/ElementRepository.php +++ b/src/Element/ElementRepository.php @@ -6,12 +6,8 @@ use Pimcore\Model\DataObject; use Pimcore\Model\Document; -/** - * @internal - * - * @final - */ -class ElementRepository +/** @internal */ +final class ElementRepository { public function findAsset(int $id): ?Asset { diff --git a/tests/Integration/Invalidation/InvalidateAssetTest.php b/tests/Integration/Invalidation/InvalidateAssetTest.php index cd26b65..fa6f4c1 100644 --- a/tests/Integration/Invalidation/InvalidateAssetTest.php +++ b/tests/Integration/Invalidation/InvalidateAssetTest.php @@ -3,8 +3,10 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Invalidation; use FOS\HttpCacheBundle\CacheManager; +use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\ArrangeCacheTest; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestAssetFactory; +use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestObjectFactory; use Neusta\Pimcore\TestingFramework\Database\ResetDatabase; use Neusta\Pimcore\TestingFramework\Test\Attribute\ConfigureExtension; use Neusta\Pimcore\TestingFramework\Test\ConfigurableKernelTestCase; @@ -137,6 +139,27 @@ public function response_is_not_invalidated_when_asset_type_is_disabled_on_delet $this->cacheManager->invalidateTags(Argument::any())->shouldNotHaveBeenCalled(); } + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => true, + 'assets' => true, + ], + ])] + public function dependency_traversal_is_not_triggered_when_asset_is_updated(): void + { + $object = self::arrange( + fn () => TestObjectFactory::simpleObject(12, 'test_object_with_asset', [$this->asset])->save(), + ); + + $this->asset->setData('Updated test content')->save(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($object)->toString()]) + ->shouldNotHaveBeenCalled(); + } + /** * @test */ diff --git a/tests/Integration/Invalidation/InvalidateDocumentTest.php b/tests/Integration/Invalidation/InvalidateDocumentTest.php index a421e91..af5c629 100644 --- a/tests/Integration/Invalidation/InvalidateDocumentTest.php +++ b/tests/Integration/Invalidation/InvalidateDocumentTest.php @@ -250,6 +250,30 @@ public function response_is_not_invalidated_when_document_type_is_disabled_on_de $this->cacheManager->invalidateTags(Argument::any())->shouldNotHaveBeenCalled(); } + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => true, + 'documents' => true, + ], + ])] + public function dependency_traversal_is_not_triggered_when_document_is_updated(): void + { + $object = self::arrange( + fn () => TestObjectFactory::simpleObject(12)->save(), + ); + $document = self::arrange( + fn () => TestDocumentFactory::simplePage(96, 'other_test_document_page', $object)->save(), + ); + + $document->setKey('updated_other_test_document_page')->save(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($object)->toString()]) + ->shouldNotHaveBeenCalled(); + } + /** * @test */ From 096886774d2398994f6cb925354290df4975cd3f Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 07:27:00 +0100 Subject: [PATCH 07/40] Revert ElementRepository to non-final class (required for Prophecy mocking) Co-Authored-By: Claude Sonnet 4.6 --- src/Element/ElementRepository.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Element/ElementRepository.php b/src/Element/ElementRepository.php index 65ea35b..9822b0d 100644 --- a/src/Element/ElementRepository.php +++ b/src/Element/ElementRepository.php @@ -6,8 +6,12 @@ use Pimcore\Model\DataObject; use Pimcore\Model\Document; -/** @internal */ -final class ElementRepository +/** + * @internal + * + * @final + */ +class ElementRepository { public function findAsset(int $id): ?Asset { From dbbf303c2579a9cee6de679953f394721256e62a Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 07:32:47 +0100 Subject: [PATCH 08/40] =?UTF-8?q?Remove=20asset=20case=20from=20dependency?= =?UTF-8?q?=20invalidation=20=E2=80=94=20assets=20cannot=20reference=20obj?= =?UTF-8?q?ects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assets are static files (images, PDFs) and do not reference objects via Pimcore's dependency system. Even if they did, invalidating an asset's HTTP cache because referenced object data changed would be semantically wrong — the file itself is unchanged. Only objects and documents are valid dependents. Co-Authored-By: Claude Sonnet 4.6 --- src/Element/InvalidateElementListener.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index ec08bd7..20d88d0 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -65,7 +65,6 @@ private function invalidateDependencies(Dependency $dependency): void $element = match (ElementType::tryFrom($required['type'])) { ElementType::Object => $this->elementRepository->findObject($required['id']), ElementType::Document => $this->elementRepository->findDocument($required['id']), - ElementType::Asset => $this->elementRepository->findAsset($required['id']), default => null, }; From 7a02f80ac320d7ea67efeded8762c762a2c9c4bf Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 07:43:54 +0100 Subject: [PATCH 09/40] Restore asset case in dependency invalidation Standard Pimcore assets support metadata fields of type "object", which Pimcore tracks as dependencies. When an object O changes and an asset A has a metadata relation to O, O.getRequiredBy() includes A. Invalidating A's cache tag (e.g. a42) is valid: all page responses tagged with a42 (because they rendered the asset) are also purged, which is the desired cascading behaviour. Co-Authored-By: Claude Sonnet 4.6 --- src/Element/InvalidateElementListener.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index 20d88d0..ec08bd7 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -65,6 +65,7 @@ private function invalidateDependencies(Dependency $dependency): void $element = match (ElementType::tryFrom($required['type'])) { ElementType::Object => $this->elementRepository->findObject($required['id']), ElementType::Document => $this->elementRepository->findDocument($required['id']), + ElementType::Asset => $this->elementRepository->findAsset($required['id']), default => null, }; From 3ffcd0f73d7cc321d0de19ca07757b92d3a14057 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 08:19:34 +0100 Subject: [PATCH 10/40] Make dependency traversal configurable, disabled by default Adds an `invalidate_dependencies` config node (off by default) under each element type (assets, documents, objects). When enabled, a `types` sub-node controls which dependent element types get purged. The `InvalidateElementListener` now checks the config before traversing and before invalidating each dependent type, making the previously hardcoded object-cascade opt-in. Co-Authored-By: Claude Sonnet 4.6 --- config/services.php | 3 +- src/DependencyInjection/Configuration.php | 48 +++++++++++++++++++ .../NeustaPimcoreHttpCacheExtension.php | 3 ++ src/Element/InvalidateElementListener.php | 38 ++++++++++++--- .../Invalidation/InvalidateDocumentTest.php | 20 +++++++- .../Invalidation/InvalidateObjectTest.php | 20 +++++++- .../Element/InvalidateElementListenerTest.php | 19 +++++++- 7 files changed, 137 insertions(+), 14 deletions(-) diff --git a/config/services.php b/config/services.php index 792939c..36ac3a6 100644 --- a/config/services.php +++ b/config/services.php @@ -92,7 +92,8 @@ $services->set('neusta_pimcore_http_cache.element.invalidate_listener', InvalidateElementListener::class) ->arg('$cacheInvalidator', service('neusta_pimcore_http_cache.cache_invalidator')) ->arg('$dispatcher', service('event_dispatcher')) - ->arg('$elementRepository', service('.neusta_pimcore_http_cache.element.repository')); + ->arg('$elementRepository', service('.neusta_pimcore_http_cache.element.repository')) + ->arg('$config', abstract_arg('Set in the extension')); $services->set('neusta_pimcore_http_cache.data_collector', DataCollector::class) ->arg('$cacheTagCollector', service('.neusta_pimcore_http_cache.collect_tags_response_tagger')) diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 6f93ccb..88117b7 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -37,6 +37,22 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue(['folder' => false]) ->booleanPrototype()->end() ->end() + ->arrayNode('invalidate_dependencies') + ->info('Enable/disable invalidation of dependent elements when an asset is updated or deleted.') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('types') + ->info('Enable/disable invalidation of dependent element types.') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('assets')->defaultFalse()->end() + ->booleanNode('documents')->defaultFalse()->end() + ->booleanNode('objects')->defaultFalse()->end() + ->end() + ->end() + ->end() + ->end() ->end() ->end() ->arrayNode('documents') @@ -54,6 +70,22 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue(['email' => false, 'folder' => false, 'hardlink' => false]) ->booleanPrototype()->end() ->end() + ->arrayNode('invalidate_dependencies') + ->info('Enable/disable invalidation of dependent elements when a document is updated or deleted.') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('types') + ->info('Enable/disable invalidation of dependent element types.') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('assets')->defaultFalse()->end() + ->booleanNode('documents')->defaultFalse()->end() + ->booleanNode('objects')->defaultFalse()->end() + ->end() + ->end() + ->end() + ->end() ->end() ->end() ->arrayNode('objects') @@ -79,6 +111,22 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue([]) ->booleanPrototype()->end() ->end() + ->arrayNode('invalidate_dependencies') + ->info('Enable/disable invalidation of dependent elements when an object is updated or deleted.') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('types') + ->info('Enable/disable invalidation of dependent element types.') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('assets')->defaultFalse()->end() + ->booleanNode('documents')->defaultFalse()->end() + ->booleanNode('objects')->defaultFalse()->end() + ->end() + ->end() + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php b/src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php index b1dd629..9514f31 100644 --- a/src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php +++ b/src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php @@ -71,6 +71,9 @@ private function registerElements(ContainerBuilder $container, array $config): v ->addTag('kernel.event_listener', ['event' => DataObjectEvents::PRE_DELETE, 'method' => 'onDelete']); } + $container->getDefinition('neusta_pimcore_http_cache.element.invalidate_listener') + ->setArgument('$config', $config); + $container->setParameter('neusta_pimcore_http_cache.config', $config); } } diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index ec08bd7..820772b 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -10,10 +10,12 @@ final class InvalidateElementListener { + /** @param array $config */ public function __construct( private readonly CacheInvalidator $cacheInvalidator, private readonly EventDispatcherInterface $dispatcher, private readonly ElementRepository $elementRepository, + private readonly array $config, ) { } @@ -27,8 +29,9 @@ public function onUpdate(ElementEventInterface $event): void $this->invalidateElement($element); - if (ElementType::Object === ElementType::tryFrom($element->getType())) { - $this->invalidateDependencies($element->getDependencies()); + $type = ElementType::tryFrom($element->getType()); + if ($type !== null && $this->isDependencyTraversalEnabled($type)) { + $this->invalidateDependencies($element->getDependencies(), $type); } } @@ -38,8 +41,9 @@ public function onDelete(ElementEventInterface $event): void $this->invalidateElement($element); - if (ElementType::Object === ElementType::tryFrom($element->getType())) { - $this->invalidateDependencies($element->getDependencies()); + $type = ElementType::tryFrom($element->getType()); + if ($type !== null && $this->isDependencyTraversalEnabled($type)) { + $this->invalidateDependencies($element->getDependencies(), $type); } } @@ -55,18 +59,29 @@ private function invalidateElement(ElementInterface $element): void $this->cacheInvalidator->invalidate($invalidationEvent->cacheTags()); } - private function invalidateDependencies(Dependency $dependency): void + private function isDependencyTraversalEnabled(ElementType $type): bool { + return $this->config[$this->configKey($type)]['invalidate_dependencies']['enabled'] ?? false; + } + + private function invalidateDependencies(Dependency $dependency, ElementType $sourceType): void + { + $typesConfig = $this->config[$this->configKey($sourceType)]['invalidate_dependencies']['types'] ?? []; + foreach ($dependency->getRequiredBy() as $required) { if (!isset($required['id'], $required['type'])) { continue; } - $element = match (ElementType::tryFrom($required['type'])) { + $dependentType = ElementType::tryFrom($required['type']); + if ($dependentType === null || !($typesConfig[$this->configKey($dependentType)] ?? false)) { + continue; + } + + $element = match ($dependentType) { ElementType::Object => $this->elementRepository->findObject($required['id']), ElementType::Document => $this->elementRepository->findDocument($required['id']), ElementType::Asset => $this->elementRepository->findAsset($required['id']), - default => null, }; if ($element) { @@ -74,4 +89,13 @@ private function invalidateDependencies(Dependency $dependency): void } } } + + private function configKey(ElementType $type): string + { + return match ($type) { + ElementType::Asset => 'assets', + ElementType::Document => 'documents', + ElementType::Object => 'objects', + }; + } } diff --git a/tests/Integration/Invalidation/InvalidateDocumentTest.php b/tests/Integration/Invalidation/InvalidateDocumentTest.php index af5c629..afcd2b1 100644 --- a/tests/Integration/Invalidation/InvalidateDocumentTest.php +++ b/tests/Integration/Invalidation/InvalidateDocumentTest.php @@ -64,7 +64,15 @@ public function response_is_invalidated_when_document_is_updated(): void */ #[ConfigureExtension('neusta_pimcore_http_cache', [ 'elements' => [ - 'objects' => true, + 'objects' => [ + 'enabled' => true, + 'invalidate_dependencies' => [ + 'enabled' => true, + 'types' => [ + 'documents' => true, + ], + ], + ], 'documents' => true, ], ])] @@ -103,7 +111,15 @@ public function response_is_invalidated_when_document_is_deleted(): void */ #[ConfigureExtension('neusta_pimcore_http_cache', [ 'elements' => [ - 'objects' => true, + 'objects' => [ + 'enabled' => true, + 'invalidate_dependencies' => [ + 'enabled' => true, + 'types' => [ + 'documents' => true, + ], + ], + ], 'documents' => true, ], ])] diff --git a/tests/Integration/Invalidation/InvalidateObjectTest.php b/tests/Integration/Invalidation/InvalidateObjectTest.php index 182233d..221eed1 100644 --- a/tests/Integration/Invalidation/InvalidateObjectTest.php +++ b/tests/Integration/Invalidation/InvalidateObjectTest.php @@ -62,7 +62,15 @@ public function response_is_invalidated_when_object_is_updated(): void */ #[ConfigureExtension('neusta_pimcore_http_cache', [ 'elements' => [ - 'objects' => true, + 'objects' => [ + 'enabled' => true, + 'invalidate_dependencies' => [ + 'enabled' => true, + 'types' => [ + 'objects' => true, + ], + ], + ], ], ])] public function dependent_object_is_invalidated_on_object_update(): void @@ -98,7 +106,15 @@ public function response_is_invalidated_when_object_is_deleted(): void */ #[ConfigureExtension('neusta_pimcore_http_cache', [ 'elements' => [ - 'objects' => true, + 'objects' => [ + 'enabled' => true, + 'invalidate_dependencies' => [ + 'enabled' => true, + 'types' => [ + 'objects' => true, + ], + ], + ], ], ])] public function dependent_object_is_invalidated_on_object_deletion(): void diff --git a/tests/Unit/Element/InvalidateElementListenerTest.php b/tests/Unit/Element/InvalidateElementListenerTest.php index 5acf14c..1ac3770 100644 --- a/tests/Unit/Element/InvalidateElementListenerTest.php +++ b/tests/Unit/Element/InvalidateElementListenerTest.php @@ -47,6 +47,7 @@ protected function setUp(): void $this->cacheInvalidator->reveal(), $this->eventDispatcher->reveal(), $this->elementRepository->reveal(), + [], ); $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) @@ -118,6 +119,13 @@ public function onUpdate_should_invalidate_elements(ElementEventInterface $event */ public function onUpdate_should_invalidate_dependencies(): void { + $listener = new InvalidateElementListener( + $this->cacheInvalidator->reveal(), + $this->eventDispatcher->reveal(), + $this->elementRepository->reveal(), + ['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]], + ); + $element = $this->prophesize(DataObject\TestObject::class); $element->getType()->willReturn(ElementType::Object->value); $dependency = $this->prophesize(Dependency::class); @@ -130,7 +138,7 @@ public function onUpdate_should_invalidate_dependencies(): void $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); $this->elementRepository->findObject(23)->willReturn($dependentElement->reveal()); - $this->invalidateElementListener->onUpdate($event); + $listener->onUpdate($event); $this->cacheInvalidator->invalidate(Argument::which('toString', CacheTag::fromElement($dependentElement->reveal())->toString())) ->shouldHaveBeenCalledOnce(); @@ -248,6 +256,13 @@ public function onDelete_should_invalidate_elements(ElementEventInterface $event */ public function onDelete_should_invalidate_dependent_elements(): void { + $listener = new InvalidateElementListener( + $this->cacheInvalidator->reveal(), + $this->eventDispatcher->reveal(), + $this->elementRepository->reveal(), + ['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]], + ); + $element = $this->prophesize(DataObject\TestObject::class); $element->getType()->willReturn(ElementType::Object->value); $dependency = $this->prophesize(Dependency::class); @@ -260,7 +275,7 @@ public function onDelete_should_invalidate_dependent_elements(): void $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); $this->elementRepository->findObject(23)->willReturn($dependentElement->reveal()); - $this->invalidateElementListener->onDelete($event); + $listener->onDelete($event); $this->cacheInvalidator->invalidate(Argument::which('toString', CacheTag::fromElement($dependentElement->reveal())->toString())) ->shouldHaveBeenCalledOnce(); From ba1c99dcc35d32890bb37d29926dc51cb1c8186c Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 08:26:03 +0100 Subject: [PATCH 11/40] Fix review findings: int cast, test naming, wrong import, missing tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cast $required['id'] to int before passing to repository methods, since Pimcore returns DB rows with string values - Rename unit test to reflect that traversal is now config-driven, not element-type-driven - Fix wrong Pimcore\Image import in TestObjectFactory (→ Asset\Image) - Add positive integration tests for assets.invalidate_dependencies and documents.invalidate_dependencies (update and deletion) Co-Authored-By: Claude Sonnet 4.6 --- src/Element/InvalidateElementListener.php | 6 +- .../Integration/Helpers/TestObjectFactory.php | 2 +- .../Invalidation/InvalidateAssetTest.php | 58 +++++++++++++++++++ .../Invalidation/InvalidateDocumentTest.php | 58 +++++++++++++++++++ .../Element/InvalidateElementListenerTest.php | 2 +- 5 files changed, 121 insertions(+), 5 deletions(-) diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index 820772b..05d745c 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -79,9 +79,9 @@ private function invalidateDependencies(Dependency $dependency, ElementType $sou } $element = match ($dependentType) { - ElementType::Object => $this->elementRepository->findObject($required['id']), - ElementType::Document => $this->elementRepository->findDocument($required['id']), - ElementType::Asset => $this->elementRepository->findAsset($required['id']), + ElementType::Object => $this->elementRepository->findObject((int) $required['id']), + ElementType::Document => $this->elementRepository->findDocument((int) $required['id']), + ElementType::Asset => $this->elementRepository->findAsset((int) $required['id']), }; if ($element) { diff --git a/tests/Integration/Helpers/TestObjectFactory.php b/tests/Integration/Helpers/TestObjectFactory.php index 7e317c4..cda60cc 100644 --- a/tests/Integration/Helpers/TestObjectFactory.php +++ b/tests/Integration/Helpers/TestObjectFactory.php @@ -2,7 +2,7 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers; -use Pimcore\Image; +use Pimcore\Model\Asset\Image; use Pimcore\Model\DataObject; use Pimcore\Model\DataObject\AbstractObject; use Pimcore\Model\DataObject\TestObject; diff --git a/tests/Integration/Invalidation/InvalidateAssetTest.php b/tests/Integration/Invalidation/InvalidateAssetTest.php index fa6f4c1..cd739b4 100644 --- a/tests/Integration/Invalidation/InvalidateAssetTest.php +++ b/tests/Integration/Invalidation/InvalidateAssetTest.php @@ -139,6 +139,64 @@ public function response_is_not_invalidated_when_asset_type_is_disabled_on_delet $this->cacheManager->invalidateTags(Argument::any())->shouldNotHaveBeenCalled(); } + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'assets' => [ + 'enabled' => true, + 'invalidate_dependencies' => [ + 'enabled' => true, + 'types' => [ + 'objects' => true, + ], + ], + ], + 'objects' => true, + ], + ])] + public function dependent_object_is_invalidated_on_asset_update(): void + { + $object = self::arrange( + fn () => TestObjectFactory::simpleObject(12, 'test_object_with_image', [$this->image])->save(), + ); + + $this->image->setMimeType('image/png')->save(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($object)->toString()]) + ->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'assets' => [ + 'enabled' => true, + 'invalidate_dependencies' => [ + 'enabled' => true, + 'types' => [ + 'objects' => true, + ], + ], + ], + 'objects' => true, + ], + ])] + public function dependent_object_is_invalidated_on_asset_deletion(): void + { + $object = self::arrange( + fn () => TestObjectFactory::simpleObject(12, 'test_object_with_image', [$this->image])->save(), + ); + + $this->image->delete(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($object)->toString()]) + ->shouldHaveBeenCalledTimes(1); + } + /** * @test */ diff --git a/tests/Integration/Invalidation/InvalidateDocumentTest.php b/tests/Integration/Invalidation/InvalidateDocumentTest.php index afcd2b1..8c6f2c6 100644 --- a/tests/Integration/Invalidation/InvalidateDocumentTest.php +++ b/tests/Integration/Invalidation/InvalidateDocumentTest.php @@ -266,6 +266,64 @@ public function response_is_not_invalidated_when_document_type_is_disabled_on_de $this->cacheManager->invalidateTags(Argument::any())->shouldNotHaveBeenCalled(); } + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'documents' => [ + 'enabled' => true, + 'invalidate_dependencies' => [ + 'enabled' => true, + 'types' => [ + 'objects' => true, + ], + ], + ], + 'objects' => true, + ], + ])] + public function dependent_object_is_invalidated_on_document_update(): void + { + $object = self::arrange( + fn () => TestObjectFactory::simpleObject(12, 'test_object_with_page', [$this->document])->save(), + ); + + $this->document->setKey('updated_test_document_page')->save(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($object)->toString()]) + ->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'documents' => [ + 'enabled' => true, + 'invalidate_dependencies' => [ + 'enabled' => true, + 'types' => [ + 'objects' => true, + ], + ], + ], + 'objects' => true, + ], + ])] + public function dependent_object_is_invalidated_on_document_deletion(): void + { + $object = self::arrange( + fn () => TestObjectFactory::simpleObject(12, 'test_object_with_page', [$this->document])->save(), + ); + + $this->document->delete(); + + $this->cacheManager->invalidateTags([CacheTag::fromElement($object)->toString()]) + ->shouldHaveBeenCalledTimes(1); + } + /** * @test */ diff --git a/tests/Unit/Element/InvalidateElementListenerTest.php b/tests/Unit/Element/InvalidateElementListenerTest.php index 1ac3770..7762039 100644 --- a/tests/Unit/Element/InvalidateElementListenerTest.php +++ b/tests/Unit/Element/InvalidateElementListenerTest.php @@ -149,7 +149,7 @@ public function onUpdate_should_invalidate_dependencies(): void * * @dataProvider notObjectElementProvider */ - public function onUpdate_should_not_invalidate_dependencies_when_element_is_not_an_object( + public function onUpdate_should_not_invalidate_dependencies_when_traversal_is_disabled( ElementEventInterface $event, ): void { $this->invalidateElementListener->onUpdate($event); From b3b418e8c36c132327c6bed10591eb716f89f2dd Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 08:31:52 +0100 Subject: [PATCH 12/40] Document invalidate_dependencies configuration Adds invalidate_dependencies to the full config reference in doc/2 and a dedicated section in doc/3 explaining the feature, its opt-in nature, the one-level-deep traversal, and example configs for objects, assets, and documents as sources. Co-Authored-By: Claude Sonnet 4.6 --- doc/2-configuration.md | 31 ++++++++++++++++++---- doc/3-pimcore-elements.md | 55 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/doc/2-configuration.md b/doc/2-configuration.md index c8d1cdc..2475ee0 100644 --- a/doc/2-configuration.md +++ b/doc/2-configuration.md @@ -11,27 +11,48 @@ neusta_pimcore_http_cache: types: archive: false unknown: false - + + # Invalidate dependent elements when an asset changes (disabled by default) + invalidate_dependencies: + enabled: true + types: + objects: true + documents: true + # Unless you disable assets completely enabled: false - + documents: # By default, every type except "email", "folder" and "hardlink" is enabled types: link: false - + + # Invalidate dependent elements when a document changes (disabled by default) + invalidate_dependencies: + enabled: true + types: + objects: true + # Unless you disable documents completely enabled: false - + objects: # By default, every type except "folder" is enabled types: variant: false - + # By default, every data object class is enabled classes: MyDataObjectClass: false + # Invalidate dependent elements when an object changes (disabled by default) + invalidate_dependencies: + enabled: true + types: + objects: true + documents: true + assets: true + # Unless you disable data objects completely enabled: false diff --git a/doc/3-pimcore-elements.md b/doc/3-pimcore-elements.md index 16589c1..b587708 100644 --- a/doc/3-pimcore-elements.md +++ b/doc/3-pimcore-elements.md @@ -81,3 +81,58 @@ neusta_pimcore_http_cache: classes: MyDataObjectClass: false ``` + +## Dependent Element Invalidation + +When a Pimcore element is updated or deleted, other elements that reference it may also serve stale content. +For example, a document that embeds a data object will be outdated as soon as that object changes. + +By default, the bundle only invalidates the cache tag of the element that was directly changed. +Dependent element invalidation — traversing Pimcore's dependency graph to also purge referencing elements — is **disabled by default** and must be opted in via configuration. + +The dependency graph is one level deep: only elements that directly reference the changed element are invalidated, not transitive dependencies. + +### Enable dependent invalidation for objects + +The most common use case is invalidating documents and other objects that reference a changed data object: + +```yaml +neusta_pimcore_http_cache: + elements: + objects: + invalidate_dependencies: + enabled: true + types: + documents: true # invalidate documents that reference the object + objects: true # invalidate objects that reference the object + assets: false # leave assets out (default) +``` + +### Enable dependent invalidation for assets + +If an asset (e.g. an image) is referenced by objects or documents, those can be invalidated when the asset changes: + +```yaml +neusta_pimcore_http_cache: + elements: + assets: + invalidate_dependencies: + enabled: true + types: + objects: true # invalidate objects that reference the asset + documents: true # invalidate documents that reference the asset +``` + +### Enable dependent invalidation for documents + +If a document is referenced by other elements (e.g. an object with a document relation field), those elements can be invalidated when the document changes: + +```yaml +neusta_pimcore_http_cache: + elements: + documents: + invalidate_dependencies: + enabled: true + types: + objects: true # invalidate objects that reference the document +``` From c35ca2fb55649da3c0d15e23c0f2836c57bb9f11 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 08:36:31 +0100 Subject: [PATCH 13/40] Fix review findings: extract shared traversal method, clarify docs - Extract shared invalidateWithDependencies() from onUpdate/onDelete - Clarify doc/2 that enabled: false is mutually exclusive with other options - Add note to doc/3 that dependent element types must also be enabled in the main elements config for invalidation to take effect Co-Authored-By: Claude Sonnet 4.6 --- doc/2-configuration.md | 15 +++++++++------ doc/3-pimcore-elements.md | 15 ++++++++++++--- src/Element/InvalidateElementListener.php | 14 +++++--------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/doc/2-configuration.md b/doc/2-configuration.md index 2475ee0..3238f47 100644 --- a/doc/2-configuration.md +++ b/doc/2-configuration.md @@ -12,14 +12,15 @@ neusta_pimcore_http_cache: archive: false unknown: false - # Invalidate dependent elements when an asset changes (disabled by default) + # Invalidate dependent elements when an asset changes (disabled by default). + # Note: a dependent element type must also be enabled above for invalidation to take effect. invalidate_dependencies: enabled: true types: objects: true documents: true - # Unless you disable assets completely + # Or disable assets completely (mutually exclusive with the options above) enabled: false documents: @@ -27,13 +28,14 @@ neusta_pimcore_http_cache: types: link: false - # Invalidate dependent elements when a document changes (disabled by default) + # Invalidate dependent elements when a document changes (disabled by default). + # Note: a dependent element type must also be enabled above for invalidation to take effect. invalidate_dependencies: enabled: true types: objects: true - # Unless you disable documents completely + # Or disable documents completely (mutually exclusive with the options above) enabled: false objects: @@ -45,7 +47,8 @@ neusta_pimcore_http_cache: classes: MyDataObjectClass: false - # Invalidate dependent elements when an object changes (disabled by default) + # Invalidate dependent elements when an object changes (disabled by default). + # Note: a dependent element type must also be enabled above for invalidation to take effect. invalidate_dependencies: enabled: true types: @@ -53,7 +56,7 @@ neusta_pimcore_http_cache: documents: true assets: true - # Unless you disable data objects completely + # Or disable data objects completely (mutually exclusive with the options above) enabled: false # Enable/disable cache handling for custom cache types diff --git a/doc/3-pimcore-elements.md b/doc/3-pimcore-elements.md index b587708..5c0fd9e 100644 --- a/doc/3-pimcore-elements.md +++ b/doc/3-pimcore-elements.md @@ -92,9 +92,12 @@ Dependent element invalidation — traversing Pimcore's dependency graph to also The dependency graph is one level deep: only elements that directly reference the changed element are invalidated, not transitive dependencies. +> **Note:** For a dependent element type to actually be invalidated, it must also be enabled in the main `elements` configuration. For example, setting `objects.invalidate_dependencies.types.documents: true` has no effect if `documents` is disabled — the cache tag will be silently dropped. + ### Enable dependent invalidation for objects -The most common use case is invalidating documents and other objects that reference a changed data object: +The most common use case is invalidating documents and other objects that reference a changed data object. +The listed dependent types (`documents`, `objects`) must also be enabled in the `elements` configuration: ```yaml neusta_pimcore_http_cache: @@ -106,11 +109,13 @@ neusta_pimcore_http_cache: documents: true # invalidate documents that reference the object objects: true # invalidate objects that reference the object assets: false # leave assets out (default) + documents: true # must be enabled for document invalidation to take effect ``` ### Enable dependent invalidation for assets -If an asset (e.g. an image) is referenced by objects or documents, those can be invalidated when the asset changes: +If an asset (e.g. an image) is referenced by objects or documents, those can be invalidated when the asset changes. +The listed dependent types must also be enabled in the `elements` configuration: ```yaml neusta_pimcore_http_cache: @@ -121,11 +126,14 @@ neusta_pimcore_http_cache: types: objects: true # invalidate objects that reference the asset documents: true # invalidate documents that reference the asset + objects: true # must be enabled for object invalidation to take effect + documents: true # must be enabled for document invalidation to take effect ``` ### Enable dependent invalidation for documents -If a document is referenced by other elements (e.g. an object with a document relation field), those elements can be invalidated when the document changes: +If a document is referenced by other elements (e.g. an object with a document relation field), those elements can be invalidated when the document changes. +The listed dependent types must also be enabled in the `elements` configuration: ```yaml neusta_pimcore_http_cache: @@ -135,4 +143,5 @@ neusta_pimcore_http_cache: enabled: true types: objects: true # invalidate objects that reference the document + objects: true # must be enabled for object invalidation to take effect ``` diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index 05d745c..51e3299 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -25,20 +25,16 @@ public function onUpdate(ElementEventInterface $event): void return; } - $element = $event->getElement(); - - $this->invalidateElement($element); - - $type = ElementType::tryFrom($element->getType()); - if ($type !== null && $this->isDependencyTraversalEnabled($type)) { - $this->invalidateDependencies($element->getDependencies(), $type); - } + $this->invalidateWithDependencies($event->getElement()); } public function onDelete(ElementEventInterface $event): void { - $element = $event->getElement(); + $this->invalidateWithDependencies($event->getElement()); + } + private function invalidateWithDependencies(ElementInterface $element): void + { $this->invalidateElement($element); $type = ElementType::tryFrom($element->getType()); From f25af37f80c50296660763c14e2ab1df667f205e Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 08:58:26 +0100 Subject: [PATCH 14/40] Honor cancellation before dependency traversal If the source element's invalidation event is canceled, skip dependency traversal entirely. Previously, canceling the event only suppressed the cacheInvalidator call but dependency traversal still ran, potentially invalidating dependent elements the caller explicitly opted out of. Co-Authored-By: Claude Sonnet 4.6 --- src/Element/InvalidateElementListener.php | 10 +++++-- .../Element/InvalidateElementListenerTest.php | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index 51e3299..4ef1934 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -35,7 +35,9 @@ public function onDelete(ElementEventInterface $event): void private function invalidateWithDependencies(ElementInterface $element): void { - $this->invalidateElement($element); + if (!$this->invalidateElement($element)) { + return; + } $type = ElementType::tryFrom($element->getType()); if ($type !== null && $this->isDependencyTraversalEnabled($type)) { @@ -43,16 +45,18 @@ private function invalidateWithDependencies(ElementInterface $element): void } } - private function invalidateElement(ElementInterface $element): void + private function invalidateElement(ElementInterface $element): bool { $invalidationEvent = $this->dispatcher->dispatch(ElementInvalidationEvent::fromElement($element)); \assert($invalidationEvent instanceof ElementInvalidationEvent); if ($invalidationEvent->cancel) { - return; + return false; } $this->cacheInvalidator->invalidate($invalidationEvent->cacheTags()); + + return true; } private function isDependencyTraversalEnabled(ElementType $type): bool diff --git a/tests/Unit/Element/InvalidateElementListenerTest.php b/tests/Unit/Element/InvalidateElementListenerTest.php index 7762039..afba1df 100644 --- a/tests/Unit/Element/InvalidateElementListenerTest.php +++ b/tests/Unit/Element/InvalidateElementListenerTest.php @@ -144,6 +144,34 @@ public function onUpdate_should_invalidate_dependencies(): void ->shouldHaveBeenCalledOnce(); } + /** + * @test + */ + public function onUpdate_should_not_invalidate_dependencies_when_source_invalidation_is_canceled(): void + { + $listener = new InvalidateElementListener( + $this->cacheInvalidator->reveal(), + $this->eventDispatcher->reveal(), + $this->elementRepository->reveal(), + ['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]], + ); + + $element = $this->prophesize(DataObject\TestObject::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getId()->willReturn(42); + $event = new DataObjectEvent($element->reveal()); + + $invalidationEvent = ElementInvalidationEvent::fromElement($element->reveal()); + $invalidationEvent->cancel = true; + $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) + ->willReturn($invalidationEvent); + + $listener->onUpdate($event); + + $this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled(); + $this->cacheInvalidator->invalidate(Argument::any())->shouldNotHaveBeenCalled(); + } + /** * @test * From 36b225c7ede61652cd77e3ceaea886edc41905b6 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 09:03:48 +0100 Subject: [PATCH 15/40] Fix type mismatch in dependency traversal: use tryFromElement() getType() returns subtypes like "image" or "page", which ElementType::tryFrom() cannot match, silently skipping traversal for the most common element types. Add ElementType::tryFromElement() which delegates to Service::getElementType() (instanceof-based, always returns the generic type) and use it in invalidateWithDependencies() instead of tryFrom(getType()). Co-Authored-By: Claude Sonnet 4.6 --- src/Element/ElementType.php | 5 +++++ src/Element/InvalidateElementListener.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Element/ElementType.php b/src/Element/ElementType.php index fb136a5..02c6837 100644 --- a/src/Element/ElementType.php +++ b/src/Element/ElementType.php @@ -15,4 +15,9 @@ public static function fromElement(ElementInterface $element): self { return self::from(Service::getElementType($element) ?? ''); } + + public static function tryFromElement(ElementInterface $element): ?self + { + return self::tryFrom(Service::getElementType($element) ?? ''); + } } diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index 4ef1934..b9ff667 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -39,7 +39,7 @@ private function invalidateWithDependencies(ElementInterface $element): void return; } - $type = ElementType::tryFrom($element->getType()); + $type = ElementType::tryFromElement($element); if ($type !== null && $this->isDependencyTraversalEnabled($type)) { $this->invalidateDependencies($element->getDependencies(), $type); } From e022faa538c36182a3b22e76aa742d91a83f92dd Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 09:04:43 +0100 Subject: [PATCH 16/40] Run save() inside arrange() closure save() was called after arrange() returned, so the element was persisted with caching active instead of during the setup phase. Co-Authored-By: Claude Sonnet 4.6 --- .../Configuration/CollectConfigurationDataTest.php | 4 ++-- tests/Integration/Tagging/CollectTagsDataTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Integration/Configuration/CollectConfigurationDataTest.php b/tests/Integration/Configuration/CollectConfigurationDataTest.php index 32edb6a..e320dbb 100644 --- a/tests/Integration/Configuration/CollectConfigurationDataTest.php +++ b/tests/Integration/Configuration/CollectConfigurationDataTest.php @@ -40,7 +40,7 @@ protected function setUp(): void ])] public function collects_configuration_data(): void { - self::arrange(fn () => TestDocumentFactory::simplePage(5))->save(); + self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $this->client->request('GET', '/test_document_page'); $this->client->enableProfiler(); @@ -65,7 +65,7 @@ public function collects_configuration_data(): void ])] public function does_not_collect_configuration_data_when_profiler_is_disabled(): void { - self::arrange(fn () => TestDocumentFactory::simplePage(5))->save(); + self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $this->client->request('GET', '/test_document_page'); $this->client->enableProfiler(); diff --git a/tests/Integration/Tagging/CollectTagsDataTest.php b/tests/Integration/Tagging/CollectTagsDataTest.php index de3cab4..d3b0ebd 100644 --- a/tests/Integration/Tagging/CollectTagsDataTest.php +++ b/tests/Integration/Tagging/CollectTagsDataTest.php @@ -46,7 +46,7 @@ protected function setUp(): void #[ConfigureRoute(__DIR__ . '/../Fixtures/get_document_route.php')] public function collect_tags_for_type_document(): void { - self::arrange(fn () => TestDocumentFactory::simplePage(5))->save(); + self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()); $this->client->request('GET', '/test_document_page'); $this->client->enableProfiler(); From d2aa5c131014ae41a6f660e55e838e44ec24d1cb Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 09:05:17 +0100 Subject: [PATCH 17/40] Fix assertion: check additional tag o12, not primary tag o5 The object variant of response_is_tagged_with_additional_tag_when_X_is_loaded was asserting the primary element tag (o5) instead of the additional tag (o12) added by the listener, inconsistent with the asset (a29) and document (d12) variants. Co-Authored-By: Claude Sonnet 4.6 --- tests/Integration/Tagging/TagAdditionalTagTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Tagging/TagAdditionalTagTest.php b/tests/Integration/Tagging/TagAdditionalTagTest.php index 7c14bf8..1674ac3 100644 --- a/tests/Integration/Tagging/TagAdditionalTagTest.php +++ b/tests/Integration/Tagging/TagAdditionalTagTest.php @@ -121,7 +121,7 @@ public function response_is_tagged_with_additional_tag_when_object_is_loaded(): self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringContainsString('o5', $response->headers->get('X-Cache-Tags')); + self::assertStringContainsString('o12', $response->headers->get('X-Cache-Tags')); } /** From c0591a4dd3dd97ddd21e6d33231e3d9510ccbd14 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Mon, 2 Mar 2026 09:06:28 +0100 Subject: [PATCH 18/40] Fix mismatched hardlink tag assertion: check d12, not d29 The test arranges and requests hardlink id 12, so the assertion should verify d12 is absent from the response tags, not the unrelated d29. Co-Authored-By: Claude Sonnet 4.6 --- tests/Integration/Tagging/TagDocumentTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Tagging/TagDocumentTest.php b/tests/Integration/Tagging/TagDocumentTest.php index 66efe47..cda03ec 100644 --- a/tests/Integration/Tagging/TagDocumentTest.php +++ b/tests/Integration/Tagging/TagDocumentTest.php @@ -109,7 +109,7 @@ public function response_is_not_tagged_when_document_type_is_hard_link(): void self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringNotContainsString('d29', $response->headers->get('X-Cache-Tags')); + self::assertStringNotContainsString('d12', $response->headers->get('X-Cache-Tags')); } /** From dacd82e3b8fa75c73022c70aab33180cbc5a8bd0 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 08:53:25 +0100 Subject: [PATCH 19/40] Move configKey() from InvalidateElementListener to ElementType enum The mapping from element type to config key belongs on the enum itself, not on the listener that happens to need it. Co-Authored-By: Claude Sonnet 4.6 --- src/Element/ElementType.php | 9 +++++++++ src/Element/InvalidateElementListener.php | 14 +++----------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Element/ElementType.php b/src/Element/ElementType.php index 02c6837..0e8d9e4 100644 --- a/src/Element/ElementType.php +++ b/src/Element/ElementType.php @@ -20,4 +20,13 @@ public static function tryFromElement(ElementInterface $element): ?self { return self::tryFrom(Service::getElementType($element) ?? ''); } + + public function configKey(): string + { + return match ($this) { + self::Asset => 'assets', + self::Document => 'documents', + self::Object => 'objects', + }; + } } diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index b9ff667..6079b6c 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -61,12 +61,12 @@ private function invalidateElement(ElementInterface $element): bool private function isDependencyTraversalEnabled(ElementType $type): bool { - return $this->config[$this->configKey($type)]['invalidate_dependencies']['enabled'] ?? false; + return $this->config[$type->configKey()]['invalidate_dependencies']['enabled'] ?? false; } private function invalidateDependencies(Dependency $dependency, ElementType $sourceType): void { - $typesConfig = $this->config[$this->configKey($sourceType)]['invalidate_dependencies']['types'] ?? []; + $typesConfig = $this->config[$sourceType->configKey()]['invalidate_dependencies']['types'] ?? []; foreach ($dependency->getRequiredBy() as $required) { if (!isset($required['id'], $required['type'])) { @@ -74,7 +74,7 @@ private function invalidateDependencies(Dependency $dependency, ElementType $sou } $dependentType = ElementType::tryFrom($required['type']); - if ($dependentType === null || !($typesConfig[$this->configKey($dependentType)] ?? false)) { + if ($dependentType === null || !($typesConfig[$dependentType->configKey()] ?? false)) { continue; } @@ -90,12 +90,4 @@ private function invalidateDependencies(Dependency $dependency, ElementType $sou } } - private function configKey(ElementType $type): string - { - return match ($type) { - ElementType::Asset => 'assets', - ElementType::Document => 'documents', - ElementType::Object => 'objects', - }; - } } From 2438a323bcc1c30178667e7b5be34c55bd66614e Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 09:11:22 +0100 Subject: [PATCH 20/40] Extract shouldSkipInvalidation() into a private method Groups the skip conditions in one named place, making onUpdate() readable and providing an obvious extension point if more conditions are added. Co-Authored-By: Claude Sonnet 4.6 --- src/Element/InvalidateElementListener.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index 6079b6c..2fe8ff4 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -21,13 +21,18 @@ public function __construct( public function onUpdate(ElementEventInterface $event): void { - if ($event->hasArgument('saveVersionOnly') || $event->hasArgument('autoSave')) { + if ($this->shouldSkipInvalidation($event)) { return; } $this->invalidateWithDependencies($event->getElement()); } + private function shouldSkipInvalidation(ElementEventInterface $event): bool + { + return $event->hasArgument('saveVersionOnly') || $event->hasArgument('autoSave'); + } + public function onDelete(ElementEventInterface $event): void { $this->invalidateWithDependencies($event->getElement()); @@ -89,5 +94,4 @@ private function invalidateDependencies(Dependency $dependency, ElementType $sou } } } - } From f5f025688de85a68e597a73b2eb88da9978c7866 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 09:12:12 +0100 Subject: [PATCH 21/40] Introduce ElementsConfig as a compile-time config value object Replaces the raw array $config injected into InvalidateElementListener, AssetCacheTagChecker, DocumentCacheTagChecker, and ObjectCacheTagChecker. All config access is now through typed methods (isEnabled, isTypeEnabled, isDependencyTraversalEnabled, etc.) rather than nested array lookups. The DI extension sets ElementsConfig once; all consumers share the instance. Co-Authored-By: Claude Sonnet 4.6 --- config/services.php | 13 ++- .../Element/AssetCacheTagChecker.php | 10 +- .../Element/DocumentCacheTagChecker.php | 10 +- .../Element/ObjectCacheTagChecker.php | 12 +-- .../NeustaPimcoreHttpCacheExtension.php | 15 +-- src/Element/ElementsConfig.php | 97 +++++++++++++++++++ src/Element/InvalidateElementListener.php | 14 +-- .../Element/AssetCacheTagCheckerTest.php | 11 ++- .../Element/DocumentCacheTagCheckerTest.php | 11 ++- .../Element/ObjectCacheTagCheckerTest.php | 17 ++-- .../Element/InvalidateElementListenerTest.php | 9 +- 11 files changed, 151 insertions(+), 68 deletions(-) create mode 100644 src/Element/ElementsConfig.php diff --git a/config/services.php b/config/services.php index 36ac3a6..42f1527 100644 --- a/config/services.php +++ b/config/services.php @@ -18,6 +18,7 @@ use Neusta\Pimcore\HttpCacheBundle\CacheActivator; use Neusta\Pimcore\HttpCacheBundle\DataCollector; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener; use Neusta\Pimcore\HttpCacheBundle\Element\TagElementListener; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -73,17 +74,21 @@ $services->set('.neusta_pimcore_http_cache.element.repository', ElementRepository::class); + $services->set('neusta_pimcore_http_cache.elements_config', ElementsConfig::class) + ->factory([ElementsConfig::class, 'fromArray']) + ->arg('$config', []); + $services->set('neusta_pimcore_http_cache.cache_tag_checker.element.asset', AssetCacheTagChecker::class) ->arg('$repository', service('.neusta_pimcore_http_cache.element.repository')) - ->arg('$config', ['enabled' => false, 'types' => []]); + ->arg('$config', service('neusta_pimcore_http_cache.elements_config')); $services->set('neusta_pimcore_http_cache.cache_tag_checker.element.document', DocumentCacheTagChecker::class) ->arg('$repository', service('.neusta_pimcore_http_cache.element.repository')) - ->arg('$config', ['enabled' => false, 'types' => []]); + ->arg('$config', service('neusta_pimcore_http_cache.elements_config')); $services->set('neusta_pimcore_http_cache.cache_tag_checker.element.object', ObjectCacheTagChecker::class) ->arg('$repository', service('.neusta_pimcore_http_cache.element.repository')) - ->arg('$config', ['enabled' => false, 'types' => [], 'classes' => []]); + ->arg('$config', service('neusta_pimcore_http_cache.elements_config')); $services->set('neusta_pimcore_http_cache.element.tag_listener', TagElementListener::class) ->arg('$responseTagger', service('neusta_pimcore_http_cache.response_tagger')) @@ -93,7 +98,7 @@ ->arg('$cacheInvalidator', service('neusta_pimcore_http_cache.cache_invalidator')) ->arg('$dispatcher', service('event_dispatcher')) ->arg('$elementRepository', service('.neusta_pimcore_http_cache.element.repository')) - ->arg('$config', abstract_arg('Set in the extension')); + ->arg('$config', service('neusta_pimcore_http_cache.elements_config')); $services->set('neusta_pimcore_http_cache.data_collector', DataCollector::class) ->arg('$cacheTagCollector', service('.neusta_pimcore_http_cache.collect_tags_response_tagger')) diff --git a/src/Cache/CacheTagChecker/Element/AssetCacheTagChecker.php b/src/Cache/CacheTagChecker/Element/AssetCacheTagChecker.php index ad28256..e104263 100644 --- a/src/Cache/CacheTagChecker/Element/AssetCacheTagChecker.php +++ b/src/Cache/CacheTagChecker/Element/AssetCacheTagChecker.php @@ -7,15 +7,13 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheType\ElementCacheType; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; final class AssetCacheTagChecker implements CacheTagChecker { - /** - * @param array{enabled: bool, types: array} $config - */ public function __construct( private readonly ElementRepository $repository, - private readonly array $config, + private readonly ElementsConfig $config, ) { } @@ -24,7 +22,7 @@ public function isEnabled(CacheTag $tag): bool \assert($tag->type instanceof ElementCacheType, \sprintf('Cache type must be an instance of %s', ElementCacheType::class)); \assert(ElementType::Asset === $tag->type->type, \sprintf('Cache type must be "%s"', ElementType::Asset->value)); - if (!$this->config['enabled']) { + if (!$this->config->isEnabled(ElementType::Asset)) { return false; } @@ -32,6 +30,6 @@ public function isEnabled(CacheTag $tag): bool return false; } - return $this->config['types'][$asset->getType()] ?? true; + return $this->config->isTypeEnabled(ElementType::Asset, $asset->getType()); } } diff --git a/src/Cache/CacheTagChecker/Element/DocumentCacheTagChecker.php b/src/Cache/CacheTagChecker/Element/DocumentCacheTagChecker.php index 1d5fe5e..e713c82 100644 --- a/src/Cache/CacheTagChecker/Element/DocumentCacheTagChecker.php +++ b/src/Cache/CacheTagChecker/Element/DocumentCacheTagChecker.php @@ -7,15 +7,13 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheType\ElementCacheType; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; final class DocumentCacheTagChecker implements CacheTagChecker { - /** - * @param array{enabled: bool, types: array} $config - */ public function __construct( private readonly ElementRepository $repository, - private readonly array $config, + private readonly ElementsConfig $config, ) { } @@ -24,7 +22,7 @@ public function isEnabled(CacheTag $tag): bool \assert($tag->type instanceof ElementCacheType, \sprintf('Cache type must be an instance of %s', ElementCacheType::class)); \assert(ElementType::Document === $tag->type->type, \sprintf('Cache type must be "%s"', ElementType::Document->value)); - if (!$this->config['enabled']) { + if (!$this->config->isEnabled(ElementType::Document)) { return false; } @@ -32,6 +30,6 @@ public function isEnabled(CacheTag $tag): bool return false; } - return $this->config['types'][$document->getType()] ?? true; + return $this->config->isTypeEnabled(ElementType::Document, $document->getType()); } } diff --git a/src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php b/src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php index d10af59..885f982 100644 --- a/src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php +++ b/src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php @@ -7,16 +7,14 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheType\ElementCacheType; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; use Pimcore\Model\DataObject\Concrete; final class ObjectCacheTagChecker implements CacheTagChecker { - /** - * @param array{enabled: bool, types: array, classes: array} $config - */ public function __construct( private readonly ElementRepository $repository, - private readonly array $config, + private readonly ElementsConfig $config, ) { } @@ -25,7 +23,7 @@ public function isEnabled(CacheTag $tag): bool \assert($tag->type instanceof ElementCacheType, \sprintf('Cache type must be an instance of %s', ElementCacheType::class)); \assert(ElementType::Object === $tag->type->type, \sprintf('Cache type must be "%s"', ElementType::Object->value)); - if (!$this->config['enabled']) { + if (!$this->config->isEnabled(ElementType::Object)) { return false; } @@ -33,7 +31,7 @@ public function isEnabled(CacheTag $tag): bool return false; } - if (!($this->config['types'][$object->getType()] ?? true)) { + if (!$this->config->isTypeEnabled(ElementType::Object, $object->getType())) { return false; } @@ -41,6 +39,6 @@ public function isEnabled(CacheTag $tag): bool return true; } - return $this->config['classes'][$object->getClassName()] ?? true; + return $this->config->isClassEnabled($object->getClassName()); } } diff --git a/src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php b/src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php index 9514f31..ed78d6e 100644 --- a/src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php +++ b/src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php @@ -35,10 +35,10 @@ private function registerElements(ContainerBuilder $container, array $config): v $tagListener = $container->getDefinition('neusta_pimcore_http_cache.element.tag_listener'); $invalidateListener = $container->getDefinition('neusta_pimcore_http_cache.element.invalidate_listener'); - if ($config['assets']['enabled']) { - $container->getDefinition('neusta_pimcore_http_cache.cache_tag_checker.element.asset') - ->setArgument('$config', $config['assets']); + $container->getDefinition('neusta_pimcore_http_cache.elements_config') + ->setArgument('$config', $config); + if ($config['assets']['enabled']) { $tagListener ->addTag('kernel.event_listener', ['event' => AssetEvents::POST_LOAD]); @@ -48,9 +48,6 @@ private function registerElements(ContainerBuilder $container, array $config): v } if ($config['documents']['enabled']) { - $container->getDefinition('neusta_pimcore_http_cache.cache_tag_checker.element.document') - ->setArgument('$config', $config['documents']); - $tagListener ->addTag('kernel.event_listener', ['event' => DocumentEvents::POST_LOAD]); @@ -60,9 +57,6 @@ private function registerElements(ContainerBuilder $container, array $config): v } if ($config['objects']['enabled']) { - $container->getDefinition('neusta_pimcore_http_cache.cache_tag_checker.element.object') - ->setArgument('$config', $config['objects']); - $tagListener ->addTag('kernel.event_listener', ['event' => DataObjectEvents::POST_LOAD]); @@ -71,9 +65,6 @@ private function registerElements(ContainerBuilder $container, array $config): v ->addTag('kernel.event_listener', ['event' => DataObjectEvents::PRE_DELETE, 'method' => 'onDelete']); } - $container->getDefinition('neusta_pimcore_http_cache.element.invalidate_listener') - ->setArgument('$config', $config); - $container->setParameter('neusta_pimcore_http_cache.config', $config); } } diff --git a/src/Element/ElementsConfig.php b/src/Element/ElementsConfig.php new file mode 100644 index 0000000..07ff6ed --- /dev/null +++ b/src/Element/ElementsConfig.php @@ -0,0 +1,97 @@ + $assetTypes + * @param array $assetDependencyTypes + * @param array $documentTypes + * @param array $documentDependencyTypes + * @param array $objectTypes + * @param array $objectClasses + * @param array $objectDependencyTypes + */ + public function __construct( + private bool $assetsEnabled, + private array $assetTypes, + private bool $assetDependencyTraversalEnabled, + private array $assetDependencyTypes, + private bool $documentsEnabled, + private array $documentTypes, + private bool $documentDependencyTraversalEnabled, + private array $documentDependencyTypes, + private bool $objectsEnabled, + private array $objectTypes, + private array $objectClasses, + private bool $objectDependencyTraversalEnabled, + private array $objectDependencyTypes, + ) { + } + + /** @param array $config */ + public static function fromArray(array $config): self + { + return new self( + assetsEnabled: $config['assets']['enabled'] ?? false, + assetTypes: $config['assets']['types'] ?? [], + assetDependencyTraversalEnabled: $config['assets']['invalidate_dependencies']['enabled'] ?? false, + assetDependencyTypes: $config['assets']['invalidate_dependencies']['types'] ?? [], + documentsEnabled: $config['documents']['enabled'] ?? false, + documentTypes: $config['documents']['types'] ?? [], + documentDependencyTraversalEnabled: $config['documents']['invalidate_dependencies']['enabled'] ?? false, + documentDependencyTypes: $config['documents']['invalidate_dependencies']['types'] ?? [], + objectsEnabled: $config['objects']['enabled'] ?? false, + objectTypes: $config['objects']['types'] ?? [], + objectClasses: $config['objects']['classes'] ?? [], + objectDependencyTraversalEnabled: $config['objects']['invalidate_dependencies']['enabled'] ?? false, + objectDependencyTypes: $config['objects']['invalidate_dependencies']['types'] ?? [], + ); + } + + public function isEnabled(ElementType $type): bool + { + return match ($type) { + ElementType::Asset => $this->assetsEnabled, + ElementType::Document => $this->documentsEnabled, + ElementType::Object => $this->objectsEnabled, + }; + } + + public function isTypeEnabled(ElementType $elementType, string $type): bool + { + $types = match ($elementType) { + ElementType::Asset => $this->assetTypes, + ElementType::Document => $this->documentTypes, + ElementType::Object => $this->objectTypes, + }; + + return $types[$type] ?? true; + } + + public function isClassEnabled(string $class): bool + { + return $this->objectClasses[$class] ?? true; + } + + public function isDependencyTraversalEnabled(ElementType $type): bool + { + return match ($type) { + ElementType::Asset => $this->assetDependencyTraversalEnabled, + ElementType::Document => $this->documentDependencyTraversalEnabled, + ElementType::Object => $this->objectDependencyTraversalEnabled, + }; + } + + public function isDependentTypeEnabled(ElementType $source, ElementType $dependent): bool + { + $types = match ($source) { + ElementType::Asset => $this->assetDependencyTypes, + ElementType::Document => $this->documentDependencyTypes, + ElementType::Object => $this->objectDependencyTypes, + }; + + return $types[$dependent->configKey()] ?? false; + } +} diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index 2fe8ff4..4baf010 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -10,12 +10,11 @@ final class InvalidateElementListener { - /** @param array $config */ public function __construct( private readonly CacheInvalidator $cacheInvalidator, private readonly EventDispatcherInterface $dispatcher, private readonly ElementRepository $elementRepository, - private readonly array $config, + private readonly ElementsConfig $config, ) { } @@ -45,7 +44,7 @@ private function invalidateWithDependencies(ElementInterface $element): void } $type = ElementType::tryFromElement($element); - if ($type !== null && $this->isDependencyTraversalEnabled($type)) { + if ($type !== null && $this->config->isDependencyTraversalEnabled($type)) { $this->invalidateDependencies($element->getDependencies(), $type); } } @@ -64,22 +63,15 @@ private function invalidateElement(ElementInterface $element): bool return true; } - private function isDependencyTraversalEnabled(ElementType $type): bool - { - return $this->config[$type->configKey()]['invalidate_dependencies']['enabled'] ?? false; - } - private function invalidateDependencies(Dependency $dependency, ElementType $sourceType): void { - $typesConfig = $this->config[$sourceType->configKey()]['invalidate_dependencies']['types'] ?? []; - foreach ($dependency->getRequiredBy() as $required) { if (!isset($required['id'], $required['type'])) { continue; } $dependentType = ElementType::tryFrom($required['type']); - if ($dependentType === null || !($typesConfig[$dependentType->configKey()] ?? false)) { + if ($dependentType === null || !$this->config->isDependentTypeEnabled($sourceType, $dependentType)) { continue; } diff --git a/tests/Unit/Cache/CacheTagChecker/Element/AssetCacheTagCheckerTest.php b/tests/Unit/Cache/CacheTagChecker/Element/AssetCacheTagCheckerTest.php index e128923..03887d8 100644 --- a/tests/Unit/Cache/CacheTagChecker/Element/AssetCacheTagCheckerTest.php +++ b/tests/Unit/Cache/CacheTagChecker/Element/AssetCacheTagCheckerTest.php @@ -5,6 +5,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTagChecker; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; use PHPUnit\Framework\TestCase; use Pimcore\Model\Asset; use Prophecy\PhpUnit\ProphecyTrait; @@ -30,7 +31,7 @@ public function it_returns_false_when_asset_is_disabled(): void $asset = $this->prophesize(Asset::class); $elementCacheTagChecker = new CacheTagChecker\Element\AssetCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => false, 'types' => []], + config: ElementsConfig::fromArray(['assets' => ['enabled' => false, 'types' => []]]), ); $asset->getId()->willReturn(42); @@ -48,7 +49,7 @@ public function it_returns_false_when_asset_does_not_exist(): void $asset = $this->prophesize(Asset::class); $elementCacheTagChecker = new CacheTagChecker\Element\AssetCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => ['foo' => true]], + config: ElementsConfig::fromArray(['assets' => ['enabled' => true, 'types' => ['foo' => true]]]), ); $asset->getId()->willReturn(42); @@ -67,7 +68,7 @@ public function it_returns_false_when_asset_type_is_disabled(): void $asset = $this->prophesize(Asset::class); $elementCacheTagChecker = new CacheTagChecker\Element\AssetCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => ['foo' => false]], + config: ElementsConfig::fromArray(['assets' => ['enabled' => true, 'types' => ['foo' => false]]]), ); $asset->getId()->willReturn(42); @@ -87,7 +88,7 @@ public function it_returns_true_when_asset_type_is_enabled(): void $asset = $this->prophesize(Asset::class); $elementCacheTagChecker = new CacheTagChecker\Element\AssetCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => ['foo' => true]], + config: ElementsConfig::fromArray(['assets' => ['enabled' => true, 'types' => ['foo' => true]]]), ); $asset->getId()->willReturn(42); @@ -107,7 +108,7 @@ public function it_returns_true_when_asset_type_is_not_disabled(): void $asset = $this->prophesize(Asset::class); $elementCacheTagChecker = new CacheTagChecker\Element\AssetCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => ['foo' => false]], + config: ElementsConfig::fromArray(['assets' => ['enabled' => true, 'types' => ['foo' => false]]]), ); $asset->getId()->willReturn(42); diff --git a/tests/Unit/Cache/CacheTagChecker/Element/DocumentCacheTagCheckerTest.php b/tests/Unit/Cache/CacheTagChecker/Element/DocumentCacheTagCheckerTest.php index 6c9d3f7..77076fe 100644 --- a/tests/Unit/Cache/CacheTagChecker/Element/DocumentCacheTagCheckerTest.php +++ b/tests/Unit/Cache/CacheTagChecker/Element/DocumentCacheTagCheckerTest.php @@ -5,6 +5,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTagChecker\Element\DocumentCacheTagChecker; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; use PHPUnit\Framework\TestCase; use Pimcore\Model\Document; use Prophecy\PhpUnit\ProphecyTrait; @@ -30,7 +31,7 @@ public function it_returns_false_when_document_is_disabled(): void $document = $this->prophesize(Document::class); $elementCacheTagChecker = new DocumentCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => false, 'types' => []], + config: ElementsConfig::fromArray(['documents' => ['enabled' => false, 'types' => []]]), ); $document->getId()->willReturn(42); @@ -48,7 +49,7 @@ public function it_returns_false_when_document_does_not_exist(): void $document = $this->prophesize(Document::class); $elementCacheTagChecker = new DocumentCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => []], + config: ElementsConfig::fromArray(['documents' => ['enabled' => true, 'types' => []]]), ); $document->getId()->willReturn(42); @@ -67,7 +68,7 @@ public function it_returns_false_when_document_type_is_disabled(): void $document = $this->prophesize(Document::class); $elementCacheTagChecker = new DocumentCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => ['foo' => false]], + config: ElementsConfig::fromArray(['documents' => ['enabled' => true, 'types' => ['foo' => false]]]), ); $document->getId()->willReturn(42); @@ -88,7 +89,7 @@ public function it_returns_true_when_document_type_is_enabled(): void $document = $this->prophesize(Document::class); $elementCacheTagChecker = new DocumentCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => ['foo' => true]], + config: ElementsConfig::fromArray(['documents' => ['enabled' => true, 'types' => ['foo' => true]]]), ); $document->getId()->willReturn(42); @@ -109,7 +110,7 @@ public function it_returns_true_when_document_type_is_not_disabled(): void $document = $this->prophesize(Document::class); $elementCacheTagChecker = new DocumentCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => ['foo' => false]], + config: ElementsConfig::fromArray(['documents' => ['enabled' => true, 'types' => ['foo' => false]]]), ); $document->getId()->willReturn(42); diff --git a/tests/Unit/Cache/CacheTagChecker/Element/ObjectCacheTagCheckerTest.php b/tests/Unit/Cache/CacheTagChecker/Element/ObjectCacheTagCheckerTest.php index 103a6f2..f70dcfe 100644 --- a/tests/Unit/Cache/CacheTagChecker/Element/ObjectCacheTagCheckerTest.php +++ b/tests/Unit/Cache/CacheTagChecker/Element/ObjectCacheTagCheckerTest.php @@ -5,6 +5,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTagChecker\Element\ObjectCacheTagChecker; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; use PHPUnit\Framework\TestCase; use Pimcore\Model\DataObject; use Pimcore\Model\DataObject\AbstractObject; @@ -31,7 +32,7 @@ public function it_returns_false_when_object_is_disabled(): void $object = $this->prophesize(DataObject::class); $elementCacheTagChecker = new ObjectCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => false, 'types' => [], 'classes' => []], + config: ElementsConfig::fromArray(['objects' => ['enabled' => false, 'types' => [], 'classes' => []]]), ); $object->getId()->willReturn(42); @@ -49,7 +50,7 @@ public function it_returns_false_when_object_does_not_exist(): void $object = $this->prophesize(DataObject::class); $elementCacheTagChecker = new ObjectCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => [], 'classes' => []], + config: ElementsConfig::fromArray(['objects' => ['enabled' => true, 'types' => [], 'classes' => []]]), ); $object->getId()->willReturn(42); @@ -68,7 +69,7 @@ public function it_returns_false_when_object_type_is_disabled(): void $object = $this->prophesize(DataObject::class); $elementCacheTagChecker = new ObjectCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => [AbstractObject::OBJECT_TYPE_FOLDER => false], 'classes' => []], + config: ElementsConfig::fromArray(['objects' => ['enabled' => true, 'types' => [AbstractObject::OBJECT_TYPE_FOLDER => false], 'classes' => []]]), ); $object->getId()->willReturn(42); @@ -88,7 +89,7 @@ public function it_returns_true_when_object_type_is_enabled(): void $object = $this->prophesize(DataObject::class); $elementCacheTagChecker = new ObjectCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => [AbstractObject::OBJECT_TYPE_VARIANT => true], 'classes' => []], + config: ElementsConfig::fromArray(['objects' => ['enabled' => true, 'types' => [AbstractObject::OBJECT_TYPE_VARIANT => true], 'classes' => []]]), ); $object->getId()->willReturn(42); @@ -108,7 +109,7 @@ public function it_returns_true_when_object_type_is_not_disabled(): void $object = $this->prophesize(DataObject::class); $elementCacheTagChecker = new ObjectCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => [AbstractObject::OBJECT_TYPE_FOLDER => false], 'classes' => []], + config: ElementsConfig::fromArray(['objects' => ['enabled' => true, 'types' => [AbstractObject::OBJECT_TYPE_FOLDER => false], 'classes' => []]]), ); $object->getId()->willReturn(42); @@ -128,7 +129,7 @@ public function it_returns_true_when_object_is_not_concrete(): void $object = $this->prophesize(DataObject::class); $elementCacheTagChecker = new ObjectCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => [], 'classes' => []], + config: ElementsConfig::fromArray(['objects' => ['enabled' => true, 'types' => [], 'classes' => []]]), ); $object->getId()->willReturn(42); @@ -148,7 +149,7 @@ public function it_returns_true_when_class_is_not_disabled(): void $object = $this->prophesize(DataObject\Concrete::class); $elementCacheTagChecker = new ObjectCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => [], 'classes' => []], + config: ElementsConfig::fromArray(['objects' => ['enabled' => true, 'types' => [], 'classes' => []]]), ); $object->getId()->willReturn(42); @@ -169,7 +170,7 @@ public function it_returns_false_when_class_is_disabled(): void $object = $this->prophesize(DataObject\Concrete::class); $elementCacheTagChecker = new ObjectCacheTagChecker( $this->elementRepository->reveal(), - config: ['enabled' => true, 'types' => [], 'classes' => ['Foo' => false]], + config: ElementsConfig::fromArray(['objects' => ['enabled' => true, 'types' => [], 'classes' => ['Foo' => false]]]), ); $object->getId()->willReturn(42); diff --git a/tests/Unit/Element/InvalidateElementListenerTest.php b/tests/Unit/Element/InvalidateElementListenerTest.php index afba1df..7cf017b 100644 --- a/tests/Unit/Element/InvalidateElementListenerTest.php +++ b/tests/Unit/Element/InvalidateElementListenerTest.php @@ -8,6 +8,7 @@ use Neusta\Pimcore\HttpCacheBundle\Element\ElementInvalidationEvent; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener; use PHPUnit\Framework\TestCase; use Pimcore\Event\Model\AssetEvent; @@ -47,7 +48,7 @@ protected function setUp(): void $this->cacheInvalidator->reveal(), $this->eventDispatcher->reveal(), $this->elementRepository->reveal(), - [], + ElementsConfig::fromArray([]), ); $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) @@ -123,7 +124,7 @@ public function onUpdate_should_invalidate_dependencies(): void $this->cacheInvalidator->reveal(), $this->eventDispatcher->reveal(), $this->elementRepository->reveal(), - ['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]], + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); $element = $this->prophesize(DataObject\TestObject::class); @@ -153,7 +154,7 @@ public function onUpdate_should_not_invalidate_dependencies_when_source_invalida $this->cacheInvalidator->reveal(), $this->eventDispatcher->reveal(), $this->elementRepository->reveal(), - ['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]], + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); $element = $this->prophesize(DataObject\TestObject::class); @@ -288,7 +289,7 @@ public function onDelete_should_invalidate_dependent_elements(): void $this->cacheInvalidator->reveal(), $this->eventDispatcher->reveal(), $this->elementRepository->reveal(), - ['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]], + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); $element = $this->prophesize(DataObject\TestObject::class); From dc7ac152a34809d2407f0dbbf1e12f971ff1d91b Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 09:12:48 +0100 Subject: [PATCH 22/40] Address review findings: document traversal depth and cover missing cases - Document that dependency traversal is intentionally one level deep to prevent cycles - Add unit test for when the dependent element type is disabled in config - Add unit test for when the dependent element's own invalidation is canceled Co-Authored-By: Claude Sonnet 4.6 --- src/Element/InvalidateElementListener.php | 4 ++ .../Element/InvalidateElementListenerTest.php | 68 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index 4baf010..b235845 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -63,6 +63,10 @@ private function invalidateElement(ElementInterface $element): bool return true; } + /** + * Invalidates dependent elements one level deep. + * Dependencies of dependent elements are intentionally not traversed to prevent cycles. + */ private function invalidateDependencies(Dependency $dependency, ElementType $sourceType): void { foreach ($dependency->getRequiredBy() as $required) { diff --git a/tests/Unit/Element/InvalidateElementListenerTest.php b/tests/Unit/Element/InvalidateElementListenerTest.php index 7cf017b..74d57fd 100644 --- a/tests/Unit/Element/InvalidateElementListenerTest.php +++ b/tests/Unit/Element/InvalidateElementListenerTest.php @@ -173,6 +173,74 @@ public function onUpdate_should_not_invalidate_dependencies_when_source_invalida $this->cacheInvalidator->invalidate(Argument::any())->shouldNotHaveBeenCalled(); } + /** + * @test + */ + public function onUpdate_should_not_invalidate_dependencies_when_dependent_type_is_disabled(): void + { + $listener = new InvalidateElementListener( + $this->cacheInvalidator->reveal(), + $this->eventDispatcher->reveal(), + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => false]]]]), + ); + + $element = $this->prophesize(DataObject\TestObject::class); + $element->getType()->willReturn(ElementType::Object->value); + $dependency = $this->prophesize(Dependency::class); + $event = new DataObjectEvent($element->reveal()); + + $element->getId()->willReturn(42); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + + $listener->onUpdate($event); + + $this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function onUpdate_should_not_invalidate_dependent_element_when_its_invalidation_is_canceled(): void + { + $listener = new InvalidateElementListener( + $this->cacheInvalidator->reveal(), + $this->eventDispatcher->reveal(), + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ); + + $element = $this->prophesize(DataObject\TestObject::class); + $element->getType()->willReturn(ElementType::Object->value); + $dependency = $this->prophesize(Dependency::class); + $dependentElement = $this->prophesize(DataObject::class); + $event = new DataObjectEvent($element->reveal()); + + $element->getId()->willReturn(42); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentElement->getId()->willReturn(23); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + $this->elementRepository->findObject(23)->willReturn($dependentElement->reveal()); + + $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) + ->will(function (array $args) { + $event = $args[0]; + if ($event->element->getId() === 23) { + $event->cancel = true; + } + + return $event; + }); + + $listener->onUpdate($event); + + $this->cacheInvalidator->invalidate(Argument::which('toString', CacheTag::fromElement($element->reveal())->toString())) + ->shouldHaveBeenCalledOnce(); + $this->cacheInvalidator->invalidate(Argument::which('toString', CacheTag::fromElement($dependentElement->reveal())->toString())) + ->shouldNotHaveBeenCalled(); + } + /** * @test * From b434ec9c4bed229175c729525ca1baeb68d0ae02 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 09:28:47 +0100 Subject: [PATCH 23/40] Extract DependencyInvalidator class from InvalidateElementListener Move dependency traversal logic into its own dedicated class with a single public method, keeping InvalidateElementListener focused on dispatching events and delegating to the cache invalidator. Co-Authored-By: Claude Sonnet 4.6 --- config/services.php | 8 +- src/Element/DependencyInvalidator.php | 49 ++++ src/Element/InvalidateElementListener.php | 37 +-- .../Element/DependencyInvalidatorTest.php | 224 ++++++++++++++++++ .../Element/InvalidateElementListenerTest.php | 203 +++------------- 5 files changed, 308 insertions(+), 213 deletions(-) create mode 100644 src/Element/DependencyInvalidator.php create mode 100644 tests/Unit/Element/DependencyInvalidatorTest.php diff --git a/config/services.php b/config/services.php index 42f1527..3830b2b 100644 --- a/config/services.php +++ b/config/services.php @@ -17,6 +17,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\RemoveDisabledTagsResponseTagger; use Neusta\Pimcore\HttpCacheBundle\CacheActivator; use Neusta\Pimcore\HttpCacheBundle\DataCollector; +use Neusta\Pimcore\HttpCacheBundle\Element\DependencyInvalidator; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener; @@ -94,11 +95,14 @@ ->arg('$responseTagger', service('neusta_pimcore_http_cache.response_tagger')) ->arg('$dispatcher', service('event_dispatcher')); + $services->set('neusta_pimcore_http_cache.element.dependency_invalidator', DependencyInvalidator::class) + ->arg('$elementRepository', service('.neusta_pimcore_http_cache.element.repository')) + ->arg('$config', service('neusta_pimcore_http_cache.elements_config')); + $services->set('neusta_pimcore_http_cache.element.invalidate_listener', InvalidateElementListener::class) ->arg('$cacheInvalidator', service('neusta_pimcore_http_cache.cache_invalidator')) ->arg('$dispatcher', service('event_dispatcher')) - ->arg('$elementRepository', service('.neusta_pimcore_http_cache.element.repository')) - ->arg('$config', service('neusta_pimcore_http_cache.elements_config')); + ->arg('$dependencyInvalidator', service('neusta_pimcore_http_cache.element.dependency_invalidator')); $services->set('neusta_pimcore_http_cache.data_collector', DataCollector::class) ->arg('$cacheTagCollector', service('.neusta_pimcore_http_cache.collect_tags_response_tagger')) diff --git a/src/Element/DependencyInvalidator.php b/src/Element/DependencyInvalidator.php new file mode 100644 index 0000000..49677fd --- /dev/null +++ b/src/Element/DependencyInvalidator.php @@ -0,0 +1,49 @@ +config->isDependencyTraversalEnabled($type)) { + return; + } + + foreach ($source->getDependencies()->getRequiredBy() as $required) { + if (!isset($required['id'], $required['type'])) { + continue; + } + + $dependentType = ElementType::tryFrom($required['type']); + if ($dependentType === null || !$this->config->isDependentTypeEnabled($type, $dependentType)) { + continue; + } + + $element = match ($dependentType) { + ElementType::Object => $this->elementRepository->findObject((int) $required['id']), + ElementType::Document => $this->elementRepository->findDocument((int) $required['id']), + ElementType::Asset => $this->elementRepository->findAsset((int) $required['id']), + }; + + if ($element) { + $invalidate($element); + } + } + } +} diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index b235845..1402ade 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -4,7 +4,6 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheInvalidator; use Pimcore\Event\Model\ElementEventInterface; -use Pimcore\Model\Dependency; use Pimcore\Model\Element\ElementInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -13,8 +12,7 @@ final class InvalidateElementListener public function __construct( private readonly CacheInvalidator $cacheInvalidator, private readonly EventDispatcherInterface $dispatcher, - private readonly ElementRepository $elementRepository, - private readonly ElementsConfig $config, + private readonly DependencyInvalidator $dependencyInvalidator, ) { } @@ -43,10 +41,7 @@ private function invalidateWithDependencies(ElementInterface $element): void return; } - $type = ElementType::tryFromElement($element); - if ($type !== null && $this->config->isDependencyTraversalEnabled($type)) { - $this->invalidateDependencies($element->getDependencies(), $type); - } + $this->dependencyInvalidator->invalidate($element, fn ($e) => $this->invalidateElement($e)); } private function invalidateElement(ElementInterface $element): bool @@ -62,32 +57,4 @@ private function invalidateElement(ElementInterface $element): bool return true; } - - /** - * Invalidates dependent elements one level deep. - * Dependencies of dependent elements are intentionally not traversed to prevent cycles. - */ - private function invalidateDependencies(Dependency $dependency, ElementType $sourceType): void - { - foreach ($dependency->getRequiredBy() as $required) { - if (!isset($required['id'], $required['type'])) { - continue; - } - - $dependentType = ElementType::tryFrom($required['type']); - if ($dependentType === null || !$this->config->isDependentTypeEnabled($sourceType, $dependentType)) { - continue; - } - - $element = match ($dependentType) { - ElementType::Object => $this->elementRepository->findObject((int) $required['id']), - ElementType::Document => $this->elementRepository->findDocument((int) $required['id']), - ElementType::Asset => $this->elementRepository->findAsset((int) $required['id']), - }; - - if ($element) { - $this->invalidateElement($element); - } - } - } } diff --git a/tests/Unit/Element/DependencyInvalidatorTest.php b/tests/Unit/Element/DependencyInvalidatorTest.php new file mode 100644 index 0000000..2d198f3 --- /dev/null +++ b/tests/Unit/Element/DependencyInvalidatorTest.php @@ -0,0 +1,224 @@ + */ + private ObjectProphecy $elementRepository; + + protected function setUp(): void + { + $this->elementRepository = $this->prophesize(ElementRepository::class); + } + + /** + * @test + */ + public function invalidate_does_nothing_when_traversal_is_disabled_for_source_type(): void + { + $invalidator = new DependencyInvalidator( + $this->elementRepository->reveal(), + ElementsConfig::fromArray([]), + ); + + $element = $this->prophesize(TestObject::class); + $element->getType()->willReturn(ElementType::Object->value); + + $called = false; + $invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; }); + + self::assertFalse($called); + $this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function invalidate_skips_entries_without_id_or_type(): void + { + $invalidator = new DependencyInvalidator( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependency->getRequiredBy()->willReturn([ + ['type' => 'object'], // missing id + ['id' => 23], // missing type + [], // missing both + ]); + + $called = false; + $invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; }); + + self::assertFalse($called); + $this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function invalidate_skips_entries_with_unknown_type(): void + { + $invalidator = new DependencyInvalidator( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'unknown']]); + + $called = false; + $invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; }); + + self::assertFalse($called); + $this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function invalidate_skips_entries_when_dependent_type_is_disabled(): void + { + $invalidator = new DependencyInvalidator( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => false]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + + $called = false; + $invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; }); + + self::assertFalse($called); + $this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function invalidate_skips_dependent_element_when_not_found(): void + { + $invalidator = new DependencyInvalidator( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + $this->elementRepository->findObject(23)->willReturn(null); + + $called = false; + $invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; }); + + self::assertFalse($called); + } + + /** + * @test + */ + public function invalidate_calls_callable_for_each_dependent_object(): void + { + $invalidator = new DependencyInvalidator( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentElement = $this->prophesize(DataObject::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentElement->getId()->willReturn(23); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + $this->elementRepository->findObject(23)->willReturn($dependentElement->reveal()); + + $received = []; + $invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; }); + + self::assertCount(1, $received); + self::assertSame($dependentElement->reveal(), $received[0]); + } + + /** + * @test + */ + public function invalidate_calls_callable_for_dependent_document(): void + { + $invalidator = new DependencyInvalidator( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['documents' => true]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentDocument = $this->prophesize(Document::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentDocument->getId()->willReturn(5); + $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); + $this->elementRepository->findDocument(5)->willReturn($dependentDocument->reveal()); + + $received = []; + $invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; }); + + self::assertCount(1, $received); + self::assertSame($dependentDocument->reveal(), $received[0]); + } + + /** + * @test + */ + public function invalidate_calls_callable_for_dependent_asset(): void + { + $invalidator = new DependencyInvalidator( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['assets' => true]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentAsset = $this->prophesize(Asset::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentAsset->getId()->willReturn(7); + $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); + $this->elementRepository->findAsset(7)->willReturn($dependentAsset->reveal()); + + $received = []; + $invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; }); + + self::assertCount(1, $received); + self::assertSame($dependentAsset->reveal(), $received[0]); + } +} diff --git a/tests/Unit/Element/InvalidateElementListenerTest.php b/tests/Unit/Element/InvalidateElementListenerTest.php index 74d57fd..5b2c919 100644 --- a/tests/Unit/Element/InvalidateElementListenerTest.php +++ b/tests/Unit/Element/InvalidateElementListenerTest.php @@ -5,10 +5,9 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheInvalidator; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTags; +use Neusta\Pimcore\HttpCacheBundle\Element\DependencyInvalidator; use Neusta\Pimcore\HttpCacheBundle\Element\ElementInvalidationEvent; -use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; -use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener; use PHPUnit\Framework\TestCase; use Pimcore\Event\Model\AssetEvent; @@ -17,7 +16,6 @@ use Pimcore\Event\Model\ElementEventInterface; use Pimcore\Model\Asset; use Pimcore\Model\DataObject; -use Pimcore\Model\Dependency; use Pimcore\Model\Document; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -36,23 +34,23 @@ final class InvalidateElementListenerTest extends TestCase /** @var ObjectProphecy */ private $eventDispatcher; - /** @var ObjectProphecy */ - private $elementRepository; + /** @var ObjectProphecy */ + private $dependencyInvalidator; protected function setUp(): void { $this->cacheInvalidator = $this->prophesize(CacheInvalidator::class); $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); - $this->elementRepository = $this->prophesize(ElementRepository::class); + $this->dependencyInvalidator = $this->prophesize(DependencyInvalidator::class); $this->invalidateElementListener = new InvalidateElementListener( $this->cacheInvalidator->reveal(), $this->eventDispatcher->reveal(), - $this->elementRepository->reveal(), - ElementsConfig::fromArray([]), + $this->dependencyInvalidator->reveal(), ); $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) ->willReturnArgument(); + $this->dependencyInvalidator->invalidate(Argument::cetera()); } /** @@ -117,163 +115,37 @@ public function onUpdate_should_invalidate_elements(ElementEventInterface $event /** * @test + * + * @dataProvider elementProvider */ - public function onUpdate_should_invalidate_dependencies(): void - { - $listener = new InvalidateElementListener( - $this->cacheInvalidator->reveal(), - $this->eventDispatcher->reveal(), - $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), - ); - - $element = $this->prophesize(DataObject\TestObject::class); - $element->getType()->willReturn(ElementType::Object->value); - $dependency = $this->prophesize(Dependency::class); - $dependentElement = $this->prophesize(DataObject::class); - $event = new DataObjectEvent($element->reveal()); - - $element->getId()->willReturn(42); - $element->getDependencies()->willReturn($dependency->reveal()); - $dependentElement->getId()->willReturn(23); - $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); - $this->elementRepository->findObject(23)->willReturn($dependentElement->reveal()); - - $listener->onUpdate($event); - - $this->cacheInvalidator->invalidate(Argument::which('toString', CacheTag::fromElement($dependentElement->reveal())->toString())) - ->shouldHaveBeenCalledOnce(); - } - - /** - * @test - */ - public function onUpdate_should_not_invalidate_dependencies_when_source_invalidation_is_canceled(): void - { - $listener = new InvalidateElementListener( - $this->cacheInvalidator->reveal(), - $this->eventDispatcher->reveal(), - $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), - ); - - $element = $this->prophesize(DataObject\TestObject::class); - $element->getType()->willReturn(ElementType::Object->value); - $element->getId()->willReturn(42); - $event = new DataObjectEvent($element->reveal()); - - $invalidationEvent = ElementInvalidationEvent::fromElement($element->reveal()); - $invalidationEvent->cancel = true; - $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) - ->willReturn($invalidationEvent); - - $listener->onUpdate($event); - - $this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled(); - $this->cacheInvalidator->invalidate(Argument::any())->shouldNotHaveBeenCalled(); - } - - /** - * @test - */ - public function onUpdate_should_not_invalidate_dependencies_when_dependent_type_is_disabled(): void - { - $listener = new InvalidateElementListener( - $this->cacheInvalidator->reveal(), - $this->eventDispatcher->reveal(), - $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => false]]]]), - ); - - $element = $this->prophesize(DataObject\TestObject::class); - $element->getType()->willReturn(ElementType::Object->value); - $dependency = $this->prophesize(Dependency::class); - $event = new DataObjectEvent($element->reveal()); - - $element->getId()->willReturn(42); - $element->getDependencies()->willReturn($dependency->reveal()); - $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); - - $listener->onUpdate($event); - - $this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled(); - } - - /** - * @test - */ - public function onUpdate_should_not_invalidate_dependent_element_when_its_invalidation_is_canceled(): void + public function onUpdate_should_call_dependency_invalidator(ElementEventInterface $event): void { - $listener = new InvalidateElementListener( - $this->cacheInvalidator->reveal(), - $this->eventDispatcher->reveal(), - $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), - ); - - $element = $this->prophesize(DataObject\TestObject::class); - $element->getType()->willReturn(ElementType::Object->value); - $dependency = $this->prophesize(Dependency::class); - $dependentElement = $this->prophesize(DataObject::class); - $event = new DataObjectEvent($element->reveal()); - - $element->getId()->willReturn(42); - $element->getDependencies()->willReturn($dependency->reveal()); - $dependentElement->getId()->willReturn(23); - $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); - $this->elementRepository->findObject(23)->willReturn($dependentElement->reveal()); - - $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) - ->will(function (array $args) { - $event = $args[0]; - if ($event->element->getId() === 23) { - $event->cancel = true; - } - - return $event; - }); + $element = $event->getElement(); - $listener->onUpdate($event); + $this->invalidateElementListener->onUpdate($event); - $this->cacheInvalidator->invalidate(Argument::which('toString', CacheTag::fromElement($element->reveal())->toString())) + $this->dependencyInvalidator->invalidate($element, Argument::type('callable')) ->shouldHaveBeenCalledOnce(); - $this->cacheInvalidator->invalidate(Argument::which('toString', CacheTag::fromElement($dependentElement->reveal())->toString())) - ->shouldNotHaveBeenCalled(); } /** * @test * - * @dataProvider notObjectElementProvider + * @dataProvider elementProvider */ - public function onUpdate_should_not_invalidate_dependencies_when_traversal_is_disabled( + public function onUpdate_should_not_call_dependency_invalidator_when_source_invalidation_is_canceled( ElementEventInterface $event, ): void { - $this->invalidateElementListener->onUpdate($event); + $element = $event->getElement(); + $invalidationEvent = ElementInvalidationEvent::fromElement($element); + $invalidationEvent->cancel = true; - $this->cacheInvalidator->invalidate(Argument::any()) - ->shouldHaveBeenCalledOnce(); - $this->elementRepository->findObject(Argument::any()) - ->shouldNotHaveBeenCalled(); - } + $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) + ->willReturn($invalidationEvent); - public function notObjectElementProvider(): iterable - { - $asset = $this->prophesize(Asset::class); - $dependency = $this->prophesize(Dependency::class); - $asset->getId()->willReturn(42); - $asset->getType()->willReturn(ElementType::Asset->value); - $asset->getDependencies()->willReturn($dependency->reveal()); - $dependency->getRequiredBy()->willReturn(['id' => 23, 'type' => 'object']); - yield 'Asset' => ['event' => new AssetEvent($asset->reveal())]; + $this->invalidateElementListener->onUpdate($event); - $document = $this->prophesize(Document::class); - $dependency = $this->prophesize(Dependency::class); - $document->getId()->willReturn(42); - $document->getType()->willReturn(ElementType::Document->value); - $document->getDependencies()->willReturn($dependency->reveal()); - $dependency->getRequiredBy()->willReturn(['id' => 23, 'type' => 'object']); - yield 'Document' => ['event' => new DocumentEvent($document->reveal())]; + $this->dependencyInvalidator->invalidate(Argument::cetera())->shouldNotHaveBeenCalled(); } /** @@ -350,31 +222,16 @@ public function onDelete_should_invalidate_elements(ElementEventInterface $event /** * @test + * + * @dataProvider elementProvider */ - public function onDelete_should_invalidate_dependent_elements(): void + public function onDelete_should_call_dependency_invalidator(ElementEventInterface $event): void { - $listener = new InvalidateElementListener( - $this->cacheInvalidator->reveal(), - $this->eventDispatcher->reveal(), - $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), - ); - - $element = $this->prophesize(DataObject\TestObject::class); - $element->getType()->willReturn(ElementType::Object->value); - $dependency = $this->prophesize(Dependency::class); - $dependentElement = $this->prophesize(DataObject::class); - $event = new DataObjectEvent($element->reveal()); - - $element->getId()->willReturn(42); - $element->getDependencies()->willReturn($dependency->reveal()); - $dependentElement->getId()->willReturn(23); - $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); - $this->elementRepository->findObject(23)->willReturn($dependentElement->reveal()); + $element = $event->getElement(); - $listener->onDelete($event); + $this->invalidateElementListener->onDelete($event); - $this->cacheInvalidator->invalidate(Argument::which('toString', CacheTag::fromElement($dependentElement->reveal())->toString())) + $this->dependencyInvalidator->invalidate($element, Argument::type('callable')) ->shouldHaveBeenCalledOnce(); } @@ -424,24 +281,18 @@ public function onDelete_should_invalidate_additional_tags_when_requested(Elemen public function elementProvider(): iterable { - $dependency = $this->prophesize(Dependency::class); - $asset = $this->prophesize(Asset::class); $asset->getId()->willReturn(42); - $asset->getDependencies()->willReturn($dependency->reveal()); $asset->getType()->willReturn(ElementType::Asset->value); yield 'Asset' => ['event' => new AssetEvent($asset->reveal())]; $document = $this->prophesize(Document::class); $document->getId()->willReturn(42); - $document->getDependencies()->willReturn($dependency->reveal()); $document->getType()->willReturn(ElementType::Document->value); yield 'Document' => ['event' => new DocumentEvent($document->reveal())]; $dataObject = $this->prophesize(DataObject::class); $dataObject->getId()->willReturn(42); - $dataObject->getDependencies()->willReturn($dependency->reveal()); - $dependency->getRequiredBy()->willReturn([]); $dataObject->getType()->willReturn(ElementType::Object->value); yield 'Object' => ['event' => new DataObjectEvent($dataObject->reveal())]; } From 00f70abda488abf147e44e9a9da90f75d7f9464e Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 09:47:05 +0100 Subject: [PATCH 24/40] Address review findings: fix callable PHPDoc and add missing tests - Fix @param return type annotation: callable returns mixed, not bool - Add onDelete cancellation test for DependencyInvalidator delegation - Add multi-dependent test to verify the foreach processes all entries Co-Authored-By: Claude Sonnet 4.6 --- src/Element/DependencyInvalidator.php | 2 +- .../Element/DependencyInvalidatorTest.php | 33 +++++++++++++++++++ .../Element/InvalidateElementListenerTest.php | 20 +++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/Element/DependencyInvalidator.php b/src/Element/DependencyInvalidator.php index 49677fd..07a1cf0 100644 --- a/src/Element/DependencyInvalidator.php +++ b/src/Element/DependencyInvalidator.php @@ -16,7 +16,7 @@ public function __construct( * Invalidates dependent elements one level deep. * Dependencies of dependent elements are intentionally not traversed to prevent cycles. * - * @param callable(ElementInterface): bool $invalidate + * @param callable(ElementInterface): mixed $invalidate */ public function invalidate(ElementInterface $source, callable $invalidate): void { diff --git a/tests/Unit/Element/DependencyInvalidatorTest.php b/tests/Unit/Element/DependencyInvalidatorTest.php index 2d198f3..841d022 100644 --- a/tests/Unit/Element/DependencyInvalidatorTest.php +++ b/tests/Unit/Element/DependencyInvalidatorTest.php @@ -170,6 +170,39 @@ public function invalidate_calls_callable_for_each_dependent_object(): void self::assertSame($dependentElement->reveal(), $received[0]); } + /** + * @test + */ + public function invalidate_calls_callable_for_all_dependents(): void + { + $invalidator = new DependencyInvalidator( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependent1 = $this->prophesize(DataObject::class); + $dependent2 = $this->prophesize(DataObject::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependent1->getId()->willReturn(11); + $dependent2->getId()->willReturn(22); + $dependency->getRequiredBy()->willReturn([ + ['id' => 11, 'type' => 'object'], + ['id' => 22, 'type' => 'object'], + ]); + $this->elementRepository->findObject(11)->willReturn($dependent1->reveal()); + $this->elementRepository->findObject(22)->willReturn($dependent2->reveal()); + + $received = []; + $invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; }); + + self::assertCount(2, $received); + self::assertSame($dependent1->reveal(), $received[0]); + self::assertSame($dependent2->reveal(), $received[1]); + } + /** * @test */ diff --git a/tests/Unit/Element/InvalidateElementListenerTest.php b/tests/Unit/Element/InvalidateElementListenerTest.php index 5b2c919..7b2d20f 100644 --- a/tests/Unit/Element/InvalidateElementListenerTest.php +++ b/tests/Unit/Element/InvalidateElementListenerTest.php @@ -235,6 +235,26 @@ public function onDelete_should_call_dependency_invalidator(ElementEventInterfac ->shouldHaveBeenCalledOnce(); } + /** + * @test + * + * @dataProvider elementProvider + */ + public function onDelete_should_not_call_dependency_invalidator_when_source_invalidation_is_canceled( + ElementEventInterface $event, + ): void { + $element = $event->getElement(); + $invalidationEvent = ElementInvalidationEvent::fromElement($element); + $invalidationEvent->cancel = true; + + $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) + ->willReturn($invalidationEvent); + + $this->invalidateElementListener->onDelete($event); + + $this->dependencyInvalidator->invalidate(Argument::cetera())->shouldNotHaveBeenCalled(); + } + /** * @test * From 778c512fe8ba2526b9270ccbdbbe2a36495dc70b Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 10:31:55 +0100 Subject: [PATCH 25/40] Form a domain around 'dependent element' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the mixed 'dependency traversal' / 'dependent' / 'DependencyInvalidator' naming with a consistent 'dependent element' vocabulary throughout: - DependencyInvalidator → DependentElementInvalidator - isDependencyTraversalEnabled() → isDependentElementsEnabled() - isDependentTypeEnabled() → isDependentAssetInvalidationEnabled() / isDependentDocumentInvalidationEnabled() / isDependentObjectInvalidationEnabled() - $assetDependencyTraversalEnabled → $assetDependentElementsEnabled - $assetDependencyTypes → $assetDependentElementTypes - Config key: invalidate_dependencies → invalidate_dependent_elements - Service ID: .dependency_invalidator → .dependent_element_invalidator - Test: DependencyInvalidatorTest → DependentElementInvalidatorTest Co-Authored-By: Claude Sonnet 4.6 --- config/services.php | 6 +- src/DependencyInjection/Configuration.php | 6 +- ...or.php => DependentElementInvalidator.php} | 20 ++++-- src/Element/ElementsConfig.php | 66 +++++++++++-------- src/Element/InvalidateElementListener.php | 10 +-- .../Invalidation/InvalidateAssetTest.php | 6 +- .../Invalidation/InvalidateDocumentTest.php | 10 +-- .../Invalidation/InvalidateObjectTest.php | 4 +- ...hp => DependentElementInvalidatorTest.php} | 44 ++++++------- .../Element/InvalidateElementListenerTest.php | 28 ++++---- 10 files changed, 112 insertions(+), 88 deletions(-) rename src/Element/{DependencyInvalidator.php => DependentElementInvalidator.php} (70%) rename tests/Unit/Element/{DependencyInvalidatorTest.php => DependentElementInvalidatorTest.php} (85%) diff --git a/config/services.php b/config/services.php index 3830b2b..5f6f241 100644 --- a/config/services.php +++ b/config/services.php @@ -17,7 +17,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\RemoveDisabledTagsResponseTagger; use Neusta\Pimcore\HttpCacheBundle\CacheActivator; use Neusta\Pimcore\HttpCacheBundle\DataCollector; -use Neusta\Pimcore\HttpCacheBundle\Element\DependencyInvalidator; +use Neusta\Pimcore\HttpCacheBundle\Element\DependentElementInvalidator; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener; @@ -95,14 +95,14 @@ ->arg('$responseTagger', service('neusta_pimcore_http_cache.response_tagger')) ->arg('$dispatcher', service('event_dispatcher')); - $services->set('neusta_pimcore_http_cache.element.dependency_invalidator', DependencyInvalidator::class) + $services->set('neusta_pimcore_http_cache.element.dependent_element_invalidator', DependentElementInvalidator::class) ->arg('$elementRepository', service('.neusta_pimcore_http_cache.element.repository')) ->arg('$config', service('neusta_pimcore_http_cache.elements_config')); $services->set('neusta_pimcore_http_cache.element.invalidate_listener', InvalidateElementListener::class) ->arg('$cacheInvalidator', service('neusta_pimcore_http_cache.cache_invalidator')) ->arg('$dispatcher', service('event_dispatcher')) - ->arg('$dependencyInvalidator', service('neusta_pimcore_http_cache.element.dependency_invalidator')); + ->arg('$dependentElementInvalidator', service('neusta_pimcore_http_cache.element.dependent_element_invalidator')); $services->set('neusta_pimcore_http_cache.data_collector', DataCollector::class) ->arg('$cacheTagCollector', service('.neusta_pimcore_http_cache.collect_tags_response_tagger')) diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 88117b7..b456a64 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -37,7 +37,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue(['folder' => false]) ->booleanPrototype()->end() ->end() - ->arrayNode('invalidate_dependencies') + ->arrayNode('invalidate_dependent_elements') ->info('Enable/disable invalidation of dependent elements when an asset is updated or deleted.') ->canBeEnabled() ->addDefaultsIfNotSet() @@ -70,7 +70,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue(['email' => false, 'folder' => false, 'hardlink' => false]) ->booleanPrototype()->end() ->end() - ->arrayNode('invalidate_dependencies') + ->arrayNode('invalidate_dependent_elements') ->info('Enable/disable invalidation of dependent elements when a document is updated or deleted.') ->canBeEnabled() ->addDefaultsIfNotSet() @@ -111,7 +111,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue([]) ->booleanPrototype()->end() ->end() - ->arrayNode('invalidate_dependencies') + ->arrayNode('invalidate_dependent_elements') ->info('Enable/disable invalidation of dependent elements when an object is updated or deleted.') ->canBeEnabled() ->addDefaultsIfNotSet() diff --git a/src/Element/DependencyInvalidator.php b/src/Element/DependentElementInvalidator.php similarity index 70% rename from src/Element/DependencyInvalidator.php rename to src/Element/DependentElementInvalidator.php index 07a1cf0..e4b66ab 100644 --- a/src/Element/DependencyInvalidator.php +++ b/src/Element/DependentElementInvalidator.php @@ -4,7 +4,7 @@ use Pimcore\Model\Element\ElementInterface; -final class DependencyInvalidator +final class DependentElementInvalidator { public function __construct( private readonly ElementRepository $elementRepository, @@ -21,7 +21,7 @@ public function __construct( public function invalidate(ElementInterface $source, callable $invalidate): void { $type = ElementType::tryFromElement($source); - if ($type === null || !$this->config->isDependencyTraversalEnabled($type)) { + if ($type === null || !$this->config->isDependentElementsEnabled($type)) { return; } @@ -31,14 +31,24 @@ public function invalidate(ElementInterface $source, callable $invalidate): void } $dependentType = ElementType::tryFrom($required['type']); - if ($dependentType === null || !$this->config->isDependentTypeEnabled($type, $dependentType)) { + if ($dependentType === null) { + continue; + } + + $enabled = match ($dependentType) { + ElementType::Asset => $this->config->isDependentAssetInvalidationEnabled($type), + ElementType::Document => $this->config->isDependentDocumentInvalidationEnabled($type), + ElementType::Object => $this->config->isDependentObjectInvalidationEnabled($type), + }; + + if (!$enabled) { continue; } $element = match ($dependentType) { - ElementType::Object => $this->elementRepository->findObject((int) $required['id']), - ElementType::Document => $this->elementRepository->findDocument((int) $required['id']), ElementType::Asset => $this->elementRepository->findAsset((int) $required['id']), + ElementType::Document => $this->elementRepository->findDocument((int) $required['id']), + ElementType::Object => $this->elementRepository->findObject((int) $required['id']), }; if ($element) { diff --git a/src/Element/ElementsConfig.php b/src/Element/ElementsConfig.php index 07ff6ed..a3e84cb 100644 --- a/src/Element/ElementsConfig.php +++ b/src/Element/ElementsConfig.php @@ -6,27 +6,27 @@ { /** * @param array $assetTypes - * @param array $assetDependencyTypes + * @param array $assetDependentElementTypes * @param array $documentTypes - * @param array $documentDependencyTypes + * @param array $documentDependentElementTypes * @param array $objectTypes * @param array $objectClasses - * @param array $objectDependencyTypes + * @param array $objectDependentElementTypes */ public function __construct( private bool $assetsEnabled, private array $assetTypes, - private bool $assetDependencyTraversalEnabled, - private array $assetDependencyTypes, + private bool $assetDependentElementsEnabled, + private array $assetDependentElementTypes, private bool $documentsEnabled, private array $documentTypes, - private bool $documentDependencyTraversalEnabled, - private array $documentDependencyTypes, + private bool $documentDependentElementsEnabled, + private array $documentDependentElementTypes, private bool $objectsEnabled, private array $objectTypes, private array $objectClasses, - private bool $objectDependencyTraversalEnabled, - private array $objectDependencyTypes, + private bool $objectDependentElementsEnabled, + private array $objectDependentElementTypes, ) { } @@ -36,17 +36,17 @@ public static function fromArray(array $config): self return new self( assetsEnabled: $config['assets']['enabled'] ?? false, assetTypes: $config['assets']['types'] ?? [], - assetDependencyTraversalEnabled: $config['assets']['invalidate_dependencies']['enabled'] ?? false, - assetDependencyTypes: $config['assets']['invalidate_dependencies']['types'] ?? [], + assetDependentElementsEnabled: $config['assets']['invalidate_dependent_elements']['enabled'] ?? false, + assetDependentElementTypes: $config['assets']['invalidate_dependent_elements']['types'] ?? [], documentsEnabled: $config['documents']['enabled'] ?? false, documentTypes: $config['documents']['types'] ?? [], - documentDependencyTraversalEnabled: $config['documents']['invalidate_dependencies']['enabled'] ?? false, - documentDependencyTypes: $config['documents']['invalidate_dependencies']['types'] ?? [], + documentDependentElementsEnabled: $config['documents']['invalidate_dependent_elements']['enabled'] ?? false, + documentDependentElementTypes: $config['documents']['invalidate_dependent_elements']['types'] ?? [], objectsEnabled: $config['objects']['enabled'] ?? false, objectTypes: $config['objects']['types'] ?? [], objectClasses: $config['objects']['classes'] ?? [], - objectDependencyTraversalEnabled: $config['objects']['invalidate_dependencies']['enabled'] ?? false, - objectDependencyTypes: $config['objects']['invalidate_dependencies']['types'] ?? [], + objectDependentElementsEnabled: $config['objects']['invalidate_dependent_elements']['enabled'] ?? false, + objectDependentElementTypes: $config['objects']['invalidate_dependent_elements']['types'] ?? [], ); } @@ -75,23 +75,37 @@ public function isClassEnabled(string $class): bool return $this->objectClasses[$class] ?? true; } - public function isDependencyTraversalEnabled(ElementType $type): bool + public function isDependentElementsEnabled(ElementType $type): bool { return match ($type) { - ElementType::Asset => $this->assetDependencyTraversalEnabled, - ElementType::Document => $this->documentDependencyTraversalEnabled, - ElementType::Object => $this->objectDependencyTraversalEnabled, + ElementType::Asset => $this->assetDependentElementsEnabled, + ElementType::Document => $this->documentDependentElementsEnabled, + ElementType::Object => $this->objectDependentElementsEnabled, }; } - public function isDependentTypeEnabled(ElementType $source, ElementType $dependent): bool + public function isDependentAssetInvalidationEnabled(ElementType $source): bool { - $types = match ($source) { - ElementType::Asset => $this->assetDependencyTypes, - ElementType::Document => $this->documentDependencyTypes, - ElementType::Object => $this->objectDependencyTypes, - }; + return $this->dependentElementTypes($source)[ElementType::Asset->configKey()] ?? false; + } + + public function isDependentDocumentInvalidationEnabled(ElementType $source): bool + { + return $this->dependentElementTypes($source)[ElementType::Document->configKey()] ?? false; + } - return $types[$dependent->configKey()] ?? false; + public function isDependentObjectInvalidationEnabled(ElementType $source): bool + { + return $this->dependentElementTypes($source)[ElementType::Object->configKey()] ?? false; + } + + /** @return array */ + private function dependentElementTypes(ElementType $source): array + { + return match ($source) { + ElementType::Asset => $this->assetDependentElementTypes, + ElementType::Document => $this->documentDependentElementTypes, + ElementType::Object => $this->objectDependentElementTypes, + }; } } diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index 1402ade..3b09ca5 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -12,7 +12,7 @@ final class InvalidateElementListener public function __construct( private readonly CacheInvalidator $cacheInvalidator, private readonly EventDispatcherInterface $dispatcher, - private readonly DependencyInvalidator $dependencyInvalidator, + private readonly DependentElementInvalidator $dependentElementInvalidator, ) { } @@ -22,7 +22,7 @@ public function onUpdate(ElementEventInterface $event): void return; } - $this->invalidateWithDependencies($event->getElement()); + $this->invalidateWithDependentElements($event->getElement()); } private function shouldSkipInvalidation(ElementEventInterface $event): bool @@ -32,16 +32,16 @@ private function shouldSkipInvalidation(ElementEventInterface $event): bool public function onDelete(ElementEventInterface $event): void { - $this->invalidateWithDependencies($event->getElement()); + $this->invalidateWithDependentElements($event->getElement()); } - private function invalidateWithDependencies(ElementInterface $element): void + private function invalidateWithDependentElements(ElementInterface $element): void { if (!$this->invalidateElement($element)) { return; } - $this->dependencyInvalidator->invalidate($element, fn ($e) => $this->invalidateElement($e)); + $this->dependentElementInvalidator->invalidate($element, fn ($e) => $this->invalidateElement($e)); } private function invalidateElement(ElementInterface $element): bool diff --git a/tests/Integration/Invalidation/InvalidateAssetTest.php b/tests/Integration/Invalidation/InvalidateAssetTest.php index cd739b4..5e42cd8 100644 --- a/tests/Integration/Invalidation/InvalidateAssetTest.php +++ b/tests/Integration/Invalidation/InvalidateAssetTest.php @@ -146,7 +146,7 @@ public function response_is_not_invalidated_when_asset_type_is_disabled_on_delet 'elements' => [ 'assets' => [ 'enabled' => true, - 'invalidate_dependencies' => [ + 'invalidate_dependent_elements' => [ 'enabled' => true, 'types' => [ 'objects' => true, @@ -175,7 +175,7 @@ public function dependent_object_is_invalidated_on_asset_update(): void 'elements' => [ 'assets' => [ 'enabled' => true, - 'invalidate_dependencies' => [ + 'invalidate_dependent_elements' => [ 'enabled' => true, 'types' => [ 'objects' => true, @@ -206,7 +206,7 @@ public function dependent_object_is_invalidated_on_asset_deletion(): void 'assets' => true, ], ])] - public function dependency_traversal_is_not_triggered_when_asset_is_updated(): void + public function dependent_elements_are_not_invalidated_when_asset_is_updated(): void { $object = self::arrange( fn () => TestObjectFactory::simpleObject(12, 'test_object_with_asset', [$this->asset])->save(), diff --git a/tests/Integration/Invalidation/InvalidateDocumentTest.php b/tests/Integration/Invalidation/InvalidateDocumentTest.php index 8c6f2c6..f0ce4bd 100644 --- a/tests/Integration/Invalidation/InvalidateDocumentTest.php +++ b/tests/Integration/Invalidation/InvalidateDocumentTest.php @@ -66,7 +66,7 @@ public function response_is_invalidated_when_document_is_updated(): void 'elements' => [ 'objects' => [ 'enabled' => true, - 'invalidate_dependencies' => [ + 'invalidate_dependent_elements' => [ 'enabled' => true, 'types' => [ 'documents' => true, @@ -113,7 +113,7 @@ public function response_is_invalidated_when_document_is_deleted(): void 'elements' => [ 'objects' => [ 'enabled' => true, - 'invalidate_dependencies' => [ + 'invalidate_dependent_elements' => [ 'enabled' => true, 'types' => [ 'documents' => true, @@ -273,7 +273,7 @@ public function response_is_not_invalidated_when_document_type_is_disabled_on_de 'elements' => [ 'documents' => [ 'enabled' => true, - 'invalidate_dependencies' => [ + 'invalidate_dependent_elements' => [ 'enabled' => true, 'types' => [ 'objects' => true, @@ -302,7 +302,7 @@ public function dependent_object_is_invalidated_on_document_update(): void 'elements' => [ 'documents' => [ 'enabled' => true, - 'invalidate_dependencies' => [ + 'invalidate_dependent_elements' => [ 'enabled' => true, 'types' => [ 'objects' => true, @@ -333,7 +333,7 @@ public function dependent_object_is_invalidated_on_document_deletion(): void 'documents' => true, ], ])] - public function dependency_traversal_is_not_triggered_when_document_is_updated(): void + public function dependent_elements_are_not_invalidated_when_document_is_updated(): void { $object = self::arrange( fn () => TestObjectFactory::simpleObject(12)->save(), diff --git a/tests/Integration/Invalidation/InvalidateObjectTest.php b/tests/Integration/Invalidation/InvalidateObjectTest.php index 221eed1..a5f0733 100644 --- a/tests/Integration/Invalidation/InvalidateObjectTest.php +++ b/tests/Integration/Invalidation/InvalidateObjectTest.php @@ -64,7 +64,7 @@ public function response_is_invalidated_when_object_is_updated(): void 'elements' => [ 'objects' => [ 'enabled' => true, - 'invalidate_dependencies' => [ + 'invalidate_dependent_elements' => [ 'enabled' => true, 'types' => [ 'objects' => true, @@ -108,7 +108,7 @@ public function response_is_invalidated_when_object_is_deleted(): void 'elements' => [ 'objects' => [ 'enabled' => true, - 'invalidate_dependencies' => [ + 'invalidate_dependent_elements' => [ 'enabled' => true, 'types' => [ 'objects' => true, diff --git a/tests/Unit/Element/DependencyInvalidatorTest.php b/tests/Unit/Element/DependentElementInvalidatorTest.php similarity index 85% rename from tests/Unit/Element/DependencyInvalidatorTest.php rename to tests/Unit/Element/DependentElementInvalidatorTest.php index 841d022..9a3c942 100644 --- a/tests/Unit/Element/DependencyInvalidatorTest.php +++ b/tests/Unit/Element/DependentElementInvalidatorTest.php @@ -2,7 +2,7 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Unit\Element; -use Neusta\Pimcore\HttpCacheBundle\Element\DependencyInvalidator; +use Neusta\Pimcore\HttpCacheBundle\Element\DependentElementInvalidator; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; @@ -16,7 +16,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -final class DependencyInvalidatorTest extends TestCase +final class DependentElementInvalidatorTest extends TestCase { use ProphecyTrait; @@ -31,9 +31,9 @@ protected function setUp(): void /** * @test */ - public function invalidate_does_nothing_when_traversal_is_disabled_for_source_type(): void + public function invalidate_does_nothing_when_dependent_elements_are_disabled(): void { - $invalidator = new DependencyInvalidator( + $invalidator = new DependentElementInvalidator( $this->elementRepository->reveal(), ElementsConfig::fromArray([]), ); @@ -53,9 +53,9 @@ public function invalidate_does_nothing_when_traversal_is_disabled_for_source_ty */ public function invalidate_skips_entries_without_id_or_type(): void { - $invalidator = new DependencyInvalidator( + $invalidator = new DependentElementInvalidator( $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); $element = $this->prophesize(TestObject::class); @@ -80,9 +80,9 @@ public function invalidate_skips_entries_without_id_or_type(): void */ public function invalidate_skips_entries_with_unknown_type(): void { - $invalidator = new DependencyInvalidator( + $invalidator = new DependentElementInvalidator( $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); $element = $this->prophesize(TestObject::class); @@ -101,11 +101,11 @@ public function invalidate_skips_entries_with_unknown_type(): void /** * @test */ - public function invalidate_skips_entries_when_dependent_type_is_disabled(): void + public function invalidate_skips_entries_when_dependent_element_type_is_disabled(): void { - $invalidator = new DependencyInvalidator( + $invalidator = new DependentElementInvalidator( $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => false]]]]), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => false]]]]), ); $element = $this->prophesize(TestObject::class); @@ -126,9 +126,9 @@ public function invalidate_skips_entries_when_dependent_type_is_disabled(): void */ public function invalidate_skips_dependent_element_when_not_found(): void { - $invalidator = new DependencyInvalidator( + $invalidator = new DependentElementInvalidator( $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); $element = $this->prophesize(TestObject::class); @@ -149,9 +149,9 @@ public function invalidate_skips_dependent_element_when_not_found(): void */ public function invalidate_calls_callable_for_each_dependent_object(): void { - $invalidator = new DependencyInvalidator( + $invalidator = new DependentElementInvalidator( $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); $element = $this->prophesize(TestObject::class); @@ -173,11 +173,11 @@ public function invalidate_calls_callable_for_each_dependent_object(): void /** * @test */ - public function invalidate_calls_callable_for_all_dependents(): void + public function invalidate_calls_callable_for_all_dependent_elements(): void { - $invalidator = new DependencyInvalidator( + $invalidator = new DependentElementInvalidator( $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); $element = $this->prophesize(TestObject::class); @@ -208,9 +208,9 @@ public function invalidate_calls_callable_for_all_dependents(): void */ public function invalidate_calls_callable_for_dependent_document(): void { - $invalidator = new DependencyInvalidator( + $invalidator = new DependentElementInvalidator( $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['documents' => true]]]]), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['documents' => true]]]]), ); $element = $this->prophesize(TestObject::class); @@ -234,9 +234,9 @@ public function invalidate_calls_callable_for_dependent_document(): void */ public function invalidate_calls_callable_for_dependent_asset(): void { - $invalidator = new DependencyInvalidator( + $invalidator = new DependentElementInvalidator( $this->elementRepository->reveal(), - ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['assets' => true]]]]), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['assets' => true]]]]), ); $element = $this->prophesize(TestObject::class); diff --git a/tests/Unit/Element/InvalidateElementListenerTest.php b/tests/Unit/Element/InvalidateElementListenerTest.php index 7b2d20f..d269694 100644 --- a/tests/Unit/Element/InvalidateElementListenerTest.php +++ b/tests/Unit/Element/InvalidateElementListenerTest.php @@ -5,7 +5,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheInvalidator; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTags; -use Neusta\Pimcore\HttpCacheBundle\Element\DependencyInvalidator; +use Neusta\Pimcore\HttpCacheBundle\Element\DependentElementInvalidator; use Neusta\Pimcore\HttpCacheBundle\Element\ElementInvalidationEvent; use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener; @@ -34,23 +34,23 @@ final class InvalidateElementListenerTest extends TestCase /** @var ObjectProphecy */ private $eventDispatcher; - /** @var ObjectProphecy */ - private $dependencyInvalidator; + /** @var ObjectProphecy */ + private $dependentElementInvalidator; protected function setUp(): void { $this->cacheInvalidator = $this->prophesize(CacheInvalidator::class); $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); - $this->dependencyInvalidator = $this->prophesize(DependencyInvalidator::class); + $this->dependentElementInvalidator = $this->prophesize(DependentElementInvalidator::class); $this->invalidateElementListener = new InvalidateElementListener( $this->cacheInvalidator->reveal(), $this->eventDispatcher->reveal(), - $this->dependencyInvalidator->reveal(), + $this->dependentElementInvalidator->reveal(), ); $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) ->willReturnArgument(); - $this->dependencyInvalidator->invalidate(Argument::cetera()); + $this->dependentElementInvalidator->invalidate(Argument::cetera()); } /** @@ -118,13 +118,13 @@ public function onUpdate_should_invalidate_elements(ElementEventInterface $event * * @dataProvider elementProvider */ - public function onUpdate_should_call_dependency_invalidator(ElementEventInterface $event): void + public function onUpdate_should_call_dependent_element_invalidator(ElementEventInterface $event): void { $element = $event->getElement(); $this->invalidateElementListener->onUpdate($event); - $this->dependencyInvalidator->invalidate($element, Argument::type('callable')) + $this->dependentElementInvalidator->invalidate($element, Argument::type('callable')) ->shouldHaveBeenCalledOnce(); } @@ -133,7 +133,7 @@ public function onUpdate_should_call_dependency_invalidator(ElementEventInterfac * * @dataProvider elementProvider */ - public function onUpdate_should_not_call_dependency_invalidator_when_source_invalidation_is_canceled( + public function onUpdate_should_not_call_dependent_element_invalidator_when_source_invalidation_is_canceled( ElementEventInterface $event, ): void { $element = $event->getElement(); @@ -145,7 +145,7 @@ public function onUpdate_should_not_call_dependency_invalidator_when_source_inva $this->invalidateElementListener->onUpdate($event); - $this->dependencyInvalidator->invalidate(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->dependentElementInvalidator->invalidate(Argument::cetera())->shouldNotHaveBeenCalled(); } /** @@ -225,13 +225,13 @@ public function onDelete_should_invalidate_elements(ElementEventInterface $event * * @dataProvider elementProvider */ - public function onDelete_should_call_dependency_invalidator(ElementEventInterface $event): void + public function onDelete_should_call_dependent_element_invalidator(ElementEventInterface $event): void { $element = $event->getElement(); $this->invalidateElementListener->onDelete($event); - $this->dependencyInvalidator->invalidate($element, Argument::type('callable')) + $this->dependentElementInvalidator->invalidate($element, Argument::type('callable')) ->shouldHaveBeenCalledOnce(); } @@ -240,7 +240,7 @@ public function onDelete_should_call_dependency_invalidator(ElementEventInterfac * * @dataProvider elementProvider */ - public function onDelete_should_not_call_dependency_invalidator_when_source_invalidation_is_canceled( + public function onDelete_should_not_call_dependent_element_invalidator_when_source_invalidation_is_canceled( ElementEventInterface $event, ): void { $element = $event->getElement(); @@ -252,7 +252,7 @@ public function onDelete_should_not_call_dependency_invalidator_when_source_inva $this->invalidateElementListener->onDelete($event); - $this->dependencyInvalidator->invalidate(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->dependentElementInvalidator->invalidate(Argument::cetera())->shouldNotHaveBeenCalled(); } /** From 4e6b8a27746423fb1532efd3531c6cea50164fbb Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 10:40:57 +0100 Subject: [PATCH 26/40] Address review findings: final ElementsConfig and missing source-type tests Co-Authored-By: Claude Sonnet 4.6 --- src/Element/ElementsConfig.php | 2 +- .../DependentElementInvalidatorTest.php | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/Element/ElementsConfig.php b/src/Element/ElementsConfig.php index a3e84cb..8e88f49 100644 --- a/src/Element/ElementsConfig.php +++ b/src/Element/ElementsConfig.php @@ -2,7 +2,7 @@ namespace Neusta\Pimcore\HttpCacheBundle\Element; -readonly class ElementsConfig +final readonly class ElementsConfig { /** * @param array $assetTypes diff --git a/tests/Unit/Element/DependentElementInvalidatorTest.php b/tests/Unit/Element/DependentElementInvalidatorTest.php index 9a3c942..3a89c65 100644 --- a/tests/Unit/Element/DependentElementInvalidatorTest.php +++ b/tests/Unit/Element/DependentElementInvalidatorTest.php @@ -254,4 +254,56 @@ public function invalidate_calls_callable_for_dependent_asset(): void self::assertCount(1, $received); self::assertSame($dependentAsset->reveal(), $received[0]); } + + /** + * @test + */ + public function invalidate_calls_callable_when_source_is_an_asset(): void + { + $invalidator = new DependentElementInvalidator( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['assets' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ); + + $element = $this->prophesize(Asset::class); + $dependency = $this->prophesize(Dependency::class); + $dependentObject = $this->prophesize(DataObject::class); + $element->getType()->willReturn(ElementType::Asset->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentObject->getId()->willReturn(9); + $dependency->getRequiredBy()->willReturn([['id' => 9, 'type' => 'object']]); + $this->elementRepository->findObject(9)->willReturn($dependentObject->reveal()); + + $received = []; + $invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; }); + + self::assertCount(1, $received); + self::assertSame($dependentObject->reveal(), $received[0]); + } + + /** + * @test + */ + public function invalidate_calls_callable_when_source_is_a_document(): void + { + $invalidator = new DependentElementInvalidator( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['documents' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), + ); + + $element = $this->prophesize(Document::class); + $dependency = $this->prophesize(Dependency::class); + $dependentObject = $this->prophesize(DataObject::class); + $element->getType()->willReturn(ElementType::Document->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentObject->getId()->willReturn(14); + $dependency->getRequiredBy()->willReturn([['id' => 14, 'type' => 'object']]); + $this->elementRepository->findObject(14)->willReturn($dependentObject->reveal()); + + $received = []; + $invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; }); + + self::assertCount(1, $received); + self::assertSame($dependentObject->reveal(), $received[0]); + } } From eab4a09a4afde4d2520e49c05deb328e156de0ca Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 10:51:17 +0100 Subject: [PATCH 27/40] Rename DependentElementInvalidator to DependentElementFinder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class never performed invalidation itself — it only resolved which elements should be invalidated. Renaming to DependentElementFinder with a findFor() method makes the responsibility clear and removes the callable indirection from InvalidateElementListener. Co-Authored-By: Claude Sonnet 4.6 --- config/services.php | 6 +- ...lidator.php => DependentElementFinder.php} | 16 ++- src/Element/InvalidateElementListener.php | 6 +- ...est.php => DependentElementFinderTest.php} | 124 ++++++++---------- .../Element/InvalidateElementListenerTest.php | 24 ++-- 5 files changed, 82 insertions(+), 94 deletions(-) rename src/Element/{DependentElementInvalidator.php => DependentElementFinder.php} (85%) rename tests/Unit/Element/{DependentElementInvalidatorTest.php => DependentElementFinderTest.php} (67%) diff --git a/config/services.php b/config/services.php index 5f6f241..148a595 100644 --- a/config/services.php +++ b/config/services.php @@ -17,7 +17,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\RemoveDisabledTagsResponseTagger; use Neusta\Pimcore\HttpCacheBundle\CacheActivator; use Neusta\Pimcore\HttpCacheBundle\DataCollector; -use Neusta\Pimcore\HttpCacheBundle\Element\DependentElementInvalidator; +use Neusta\Pimcore\HttpCacheBundle\Element\DependentElementFinder; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener; @@ -95,14 +95,14 @@ ->arg('$responseTagger', service('neusta_pimcore_http_cache.response_tagger')) ->arg('$dispatcher', service('event_dispatcher')); - $services->set('neusta_pimcore_http_cache.element.dependent_element_invalidator', DependentElementInvalidator::class) + $services->set('neusta_pimcore_http_cache.element.dependent_element_finder', DependentElementFinder::class) ->arg('$elementRepository', service('.neusta_pimcore_http_cache.element.repository')) ->arg('$config', service('neusta_pimcore_http_cache.elements_config')); $services->set('neusta_pimcore_http_cache.element.invalidate_listener', InvalidateElementListener::class) ->arg('$cacheInvalidator', service('neusta_pimcore_http_cache.cache_invalidator')) ->arg('$dispatcher', service('event_dispatcher')) - ->arg('$dependentElementInvalidator', service('neusta_pimcore_http_cache.element.dependent_element_invalidator')); + ->arg('$dependentElementFinder', service('neusta_pimcore_http_cache.element.dependent_element_finder')); $services->set('neusta_pimcore_http_cache.data_collector', DataCollector::class) ->arg('$cacheTagCollector', service('.neusta_pimcore_http_cache.collect_tags_response_tagger')) diff --git a/src/Element/DependentElementInvalidator.php b/src/Element/DependentElementFinder.php similarity index 85% rename from src/Element/DependentElementInvalidator.php rename to src/Element/DependentElementFinder.php index e4b66ab..1116167 100644 --- a/src/Element/DependentElementInvalidator.php +++ b/src/Element/DependentElementFinder.php @@ -4,7 +4,7 @@ use Pimcore\Model\Element\ElementInterface; -final class DependentElementInvalidator +final class DependentElementFinder { public function __construct( private readonly ElementRepository $elementRepository, @@ -13,18 +13,20 @@ public function __construct( } /** - * Invalidates dependent elements one level deep. + * Returns dependent elements one level deep. * Dependencies of dependent elements are intentionally not traversed to prevent cycles. * - * @param callable(ElementInterface): mixed $invalidate + * @return list */ - public function invalidate(ElementInterface $source, callable $invalidate): void + public function findFor(ElementInterface $source): array { $type = ElementType::tryFromElement($source); if ($type === null || !$this->config->isDependentElementsEnabled($type)) { - return; + return []; } + $elements = []; + foreach ($source->getDependencies()->getRequiredBy() as $required) { if (!isset($required['id'], $required['type'])) { continue; @@ -52,8 +54,10 @@ public function invalidate(ElementInterface $source, callable $invalidate): void }; if ($element) { - $invalidate($element); + $elements[] = $element; } } + + return $elements; } } diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index 3b09ca5..3a9ff39 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -12,7 +12,7 @@ final class InvalidateElementListener public function __construct( private readonly CacheInvalidator $cacheInvalidator, private readonly EventDispatcherInterface $dispatcher, - private readonly DependentElementInvalidator $dependentElementInvalidator, + private readonly DependentElementFinder $dependentElementFinder, ) { } @@ -41,7 +41,9 @@ private function invalidateWithDependentElements(ElementInterface $element): voi return; } - $this->dependentElementInvalidator->invalidate($element, fn ($e) => $this->invalidateElement($e)); + foreach ($this->dependentElementFinder->findFor($element) as $dependent) { + $this->invalidateElement($dependent); + } } private function invalidateElement(ElementInterface $element): bool diff --git a/tests/Unit/Element/DependentElementInvalidatorTest.php b/tests/Unit/Element/DependentElementFinderTest.php similarity index 67% rename from tests/Unit/Element/DependentElementInvalidatorTest.php rename to tests/Unit/Element/DependentElementFinderTest.php index 3a89c65..ef72cc9 100644 --- a/tests/Unit/Element/DependentElementInvalidatorTest.php +++ b/tests/Unit/Element/DependentElementFinderTest.php @@ -2,7 +2,7 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Unit\Element; -use Neusta\Pimcore\HttpCacheBundle\Element\DependentElementInvalidator; +use Neusta\Pimcore\HttpCacheBundle\Element\DependentElementFinder; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; @@ -16,7 +16,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -final class DependentElementInvalidatorTest extends TestCase +final class DependentElementFinderTest extends TestCase { use ProphecyTrait; @@ -31,9 +31,9 @@ protected function setUp(): void /** * @test */ - public function invalidate_does_nothing_when_dependent_elements_are_disabled(): void + public function findFor_returns_empty_when_dependent_elements_are_disabled(): void { - $invalidator = new DependentElementInvalidator( + $finder = new DependentElementFinder( $this->elementRepository->reveal(), ElementsConfig::fromArray([]), ); @@ -41,19 +41,18 @@ public function invalidate_does_nothing_when_dependent_elements_are_disabled(): $element = $this->prophesize(TestObject::class); $element->getType()->willReturn(ElementType::Object->value); - $called = false; - $invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; }); + $result = $finder->findFor($element->reveal()); - self::assertFalse($called); + self::assertSame([], $result); $this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled(); } /** * @test */ - public function invalidate_skips_entries_without_id_or_type(): void + public function findFor_skips_entries_without_id_or_type(): void { - $invalidator = new DependentElementInvalidator( + $finder = new DependentElementFinder( $this->elementRepository->reveal(), ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); @@ -68,19 +67,18 @@ public function invalidate_skips_entries_without_id_or_type(): void [], // missing both ]); - $called = false; - $invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; }); + $result = $finder->findFor($element->reveal()); - self::assertFalse($called); + self::assertSame([], $result); $this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled(); } /** * @test */ - public function invalidate_skips_entries_with_unknown_type(): void + public function findFor_skips_entries_with_unknown_type(): void { - $invalidator = new DependentElementInvalidator( + $finder = new DependentElementFinder( $this->elementRepository->reveal(), ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); @@ -91,19 +89,18 @@ public function invalidate_skips_entries_with_unknown_type(): void $element->getDependencies()->willReturn($dependency->reveal()); $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'unknown']]); - $called = false; - $invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; }); + $result = $finder->findFor($element->reveal()); - self::assertFalse($called); + self::assertSame([], $result); $this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled(); } /** * @test */ - public function invalidate_skips_entries_when_dependent_element_type_is_disabled(): void + public function findFor_skips_entries_when_dependent_element_type_is_disabled(): void { - $invalidator = new DependentElementInvalidator( + $finder = new DependentElementFinder( $this->elementRepository->reveal(), ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => false]]]]), ); @@ -114,19 +111,18 @@ public function invalidate_skips_entries_when_dependent_element_type_is_disabled $element->getDependencies()->willReturn($dependency->reveal()); $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); - $called = false; - $invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; }); + $result = $finder->findFor($element->reveal()); - self::assertFalse($called); + self::assertSame([], $result); $this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled(); } /** * @test */ - public function invalidate_skips_dependent_element_when_not_found(): void + public function findFor_skips_dependent_element_when_not_found(): void { - $invalidator = new DependentElementInvalidator( + $finder = new DependentElementFinder( $this->elementRepository->reveal(), ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); @@ -138,18 +134,17 @@ public function invalidate_skips_dependent_element_when_not_found(): void $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); $this->elementRepository->findObject(23)->willReturn(null); - $called = false; - $invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; }); + $result = $finder->findFor($element->reveal()); - self::assertFalse($called); + self::assertSame([], $result); } /** * @test */ - public function invalidate_calls_callable_for_each_dependent_object(): void + public function findFor_returns_dependent_object(): void { - $invalidator = new DependentElementInvalidator( + $finder = new DependentElementFinder( $this->elementRepository->reveal(), ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); @@ -159,23 +154,21 @@ public function invalidate_calls_callable_for_each_dependent_object(): void $dependentElement = $this->prophesize(DataObject::class); $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); - $dependentElement->getId()->willReturn(23); $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); $this->elementRepository->findObject(23)->willReturn($dependentElement->reveal()); - $received = []; - $invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; }); + $result = $finder->findFor($element->reveal()); - self::assertCount(1, $received); - self::assertSame($dependentElement->reveal(), $received[0]); + self::assertCount(1, $result); + self::assertSame($dependentElement->reveal(), $result[0]); } /** * @test */ - public function invalidate_calls_callable_for_all_dependent_elements(): void + public function findFor_returns_all_dependent_elements(): void { - $invalidator = new DependentElementInvalidator( + $finder = new DependentElementFinder( $this->elementRepository->reveal(), ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); @@ -186,8 +179,6 @@ public function invalidate_calls_callable_for_all_dependent_elements(): void $dependent2 = $this->prophesize(DataObject::class); $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); - $dependent1->getId()->willReturn(11); - $dependent2->getId()->willReturn(22); $dependency->getRequiredBy()->willReturn([ ['id' => 11, 'type' => 'object'], ['id' => 22, 'type' => 'object'], @@ -195,20 +186,19 @@ public function invalidate_calls_callable_for_all_dependent_elements(): void $this->elementRepository->findObject(11)->willReturn($dependent1->reveal()); $this->elementRepository->findObject(22)->willReturn($dependent2->reveal()); - $received = []; - $invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; }); + $result = $finder->findFor($element->reveal()); - self::assertCount(2, $received); - self::assertSame($dependent1->reveal(), $received[0]); - self::assertSame($dependent2->reveal(), $received[1]); + self::assertCount(2, $result); + self::assertSame($dependent1->reveal(), $result[0]); + self::assertSame($dependent2->reveal(), $result[1]); } /** * @test */ - public function invalidate_calls_callable_for_dependent_document(): void + public function findFor_returns_dependent_document(): void { - $invalidator = new DependentElementInvalidator( + $finder = new DependentElementFinder( $this->elementRepository->reveal(), ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['documents' => true]]]]), ); @@ -218,23 +208,21 @@ public function invalidate_calls_callable_for_dependent_document(): void $dependentDocument = $this->prophesize(Document::class); $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); - $dependentDocument->getId()->willReturn(5); $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); $this->elementRepository->findDocument(5)->willReturn($dependentDocument->reveal()); - $received = []; - $invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; }); + $result = $finder->findFor($element->reveal()); - self::assertCount(1, $received); - self::assertSame($dependentDocument->reveal(), $received[0]); + self::assertCount(1, $result); + self::assertSame($dependentDocument->reveal(), $result[0]); } /** * @test */ - public function invalidate_calls_callable_for_dependent_asset(): void + public function findFor_returns_dependent_asset(): void { - $invalidator = new DependentElementInvalidator( + $finder = new DependentElementFinder( $this->elementRepository->reveal(), ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['assets' => true]]]]), ); @@ -244,23 +232,21 @@ public function invalidate_calls_callable_for_dependent_asset(): void $dependentAsset = $this->prophesize(Asset::class); $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); - $dependentAsset->getId()->willReturn(7); $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); $this->elementRepository->findAsset(7)->willReturn($dependentAsset->reveal()); - $received = []; - $invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; }); + $result = $finder->findFor($element->reveal()); - self::assertCount(1, $received); - self::assertSame($dependentAsset->reveal(), $received[0]); + self::assertCount(1, $result); + self::assertSame($dependentAsset->reveal(), $result[0]); } /** * @test */ - public function invalidate_calls_callable_when_source_is_an_asset(): void + public function findFor_returns_dependent_element_when_source_is_an_asset(): void { - $invalidator = new DependentElementInvalidator( + $finder = new DependentElementFinder( $this->elementRepository->reveal(), ElementsConfig::fromArray(['assets' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); @@ -270,23 +256,21 @@ public function invalidate_calls_callable_when_source_is_an_asset(): void $dependentObject = $this->prophesize(DataObject::class); $element->getType()->willReturn(ElementType::Asset->value); $element->getDependencies()->willReturn($dependency->reveal()); - $dependentObject->getId()->willReturn(9); $dependency->getRequiredBy()->willReturn([['id' => 9, 'type' => 'object']]); $this->elementRepository->findObject(9)->willReturn($dependentObject->reveal()); - $received = []; - $invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; }); + $result = $finder->findFor($element->reveal()); - self::assertCount(1, $received); - self::assertSame($dependentObject->reveal(), $received[0]); + self::assertCount(1, $result); + self::assertSame($dependentObject->reveal(), $result[0]); } /** * @test */ - public function invalidate_calls_callable_when_source_is_a_document(): void + public function findFor_returns_dependent_element_when_source_is_a_document(): void { - $invalidator = new DependentElementInvalidator( + $finder = new DependentElementFinder( $this->elementRepository->reveal(), ElementsConfig::fromArray(['documents' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]]]]), ); @@ -296,14 +280,12 @@ public function invalidate_calls_callable_when_source_is_a_document(): void $dependentObject = $this->prophesize(DataObject::class); $element->getType()->willReturn(ElementType::Document->value); $element->getDependencies()->willReturn($dependency->reveal()); - $dependentObject->getId()->willReturn(14); $dependency->getRequiredBy()->willReturn([['id' => 14, 'type' => 'object']]); $this->elementRepository->findObject(14)->willReturn($dependentObject->reveal()); - $received = []; - $invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; }); + $result = $finder->findFor($element->reveal()); - self::assertCount(1, $received); - self::assertSame($dependentObject->reveal(), $received[0]); + self::assertCount(1, $result); + self::assertSame($dependentObject->reveal(), $result[0]); } } diff --git a/tests/Unit/Element/InvalidateElementListenerTest.php b/tests/Unit/Element/InvalidateElementListenerTest.php index d269694..b7c9234 100644 --- a/tests/Unit/Element/InvalidateElementListenerTest.php +++ b/tests/Unit/Element/InvalidateElementListenerTest.php @@ -5,7 +5,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheInvalidator; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTags; -use Neusta\Pimcore\HttpCacheBundle\Element\DependentElementInvalidator; +use Neusta\Pimcore\HttpCacheBundle\Element\DependentElementFinder; use Neusta\Pimcore\HttpCacheBundle\Element\ElementInvalidationEvent; use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener; @@ -34,23 +34,23 @@ final class InvalidateElementListenerTest extends TestCase /** @var ObjectProphecy */ private $eventDispatcher; - /** @var ObjectProphecy */ - private $dependentElementInvalidator; + /** @var ObjectProphecy */ + private $dependentElementFinder; protected function setUp(): void { $this->cacheInvalidator = $this->prophesize(CacheInvalidator::class); $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); - $this->dependentElementInvalidator = $this->prophesize(DependentElementInvalidator::class); + $this->dependentElementFinder = $this->prophesize(DependentElementFinder::class); $this->invalidateElementListener = new InvalidateElementListener( $this->cacheInvalidator->reveal(), $this->eventDispatcher->reveal(), - $this->dependentElementInvalidator->reveal(), + $this->dependentElementFinder->reveal(), ); $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) ->willReturnArgument(); - $this->dependentElementInvalidator->invalidate(Argument::cetera()); + $this->dependentElementFinder->findFor(Argument::any())->willReturn([]); } /** @@ -118,13 +118,13 @@ public function onUpdate_should_invalidate_elements(ElementEventInterface $event * * @dataProvider elementProvider */ - public function onUpdate_should_call_dependent_element_invalidator(ElementEventInterface $event): void + public function onUpdate_should_call_dependent_element_finder(ElementEventInterface $event): void { $element = $event->getElement(); $this->invalidateElementListener->onUpdate($event); - $this->dependentElementInvalidator->invalidate($element, Argument::type('callable')) + $this->dependentElementFinder->findFor($element) ->shouldHaveBeenCalledOnce(); } @@ -145,7 +145,7 @@ public function onUpdate_should_not_call_dependent_element_invalidator_when_sour $this->invalidateElementListener->onUpdate($event); - $this->dependentElementInvalidator->invalidate(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->dependentElementFinder->findFor(Argument::any())->shouldNotHaveBeenCalled(); } /** @@ -225,13 +225,13 @@ public function onDelete_should_invalidate_elements(ElementEventInterface $event * * @dataProvider elementProvider */ - public function onDelete_should_call_dependent_element_invalidator(ElementEventInterface $event): void + public function onDelete_should_call_dependent_element_finder(ElementEventInterface $event): void { $element = $event->getElement(); $this->invalidateElementListener->onDelete($event); - $this->dependentElementInvalidator->invalidate($element, Argument::type('callable')) + $this->dependentElementFinder->findFor($element) ->shouldHaveBeenCalledOnce(); } @@ -252,7 +252,7 @@ public function onDelete_should_not_call_dependent_element_invalidator_when_sour $this->invalidateElementListener->onDelete($event); - $this->dependentElementInvalidator->invalidate(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->dependentElementFinder->findFor(Argument::any())->shouldNotHaveBeenCalled(); } /** From 15675d16db3aa9f6ed1a87683cc485e94d72d802 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 10:55:22 +0100 Subject: [PATCH 28/40] Fix docs: update config key from invalidate_dependencies to invalidate_dependent_elements Co-Authored-By: Claude Sonnet 4.6 --- doc/2-configuration.md | 6 +++--- doc/3-pimcore-elements.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/2-configuration.md b/doc/2-configuration.md index 3238f47..e2c4c0c 100644 --- a/doc/2-configuration.md +++ b/doc/2-configuration.md @@ -14,7 +14,7 @@ neusta_pimcore_http_cache: # Invalidate dependent elements when an asset changes (disabled by default). # Note: a dependent element type must also be enabled above for invalidation to take effect. - invalidate_dependencies: + invalidate_dependent_elements: enabled: true types: objects: true @@ -30,7 +30,7 @@ neusta_pimcore_http_cache: # Invalidate dependent elements when a document changes (disabled by default). # Note: a dependent element type must also be enabled above for invalidation to take effect. - invalidate_dependencies: + invalidate_dependent_elements: enabled: true types: objects: true @@ -49,7 +49,7 @@ neusta_pimcore_http_cache: # Invalidate dependent elements when an object changes (disabled by default). # Note: a dependent element type must also be enabled above for invalidation to take effect. - invalidate_dependencies: + invalidate_dependent_elements: enabled: true types: objects: true diff --git a/doc/3-pimcore-elements.md b/doc/3-pimcore-elements.md index 5c0fd9e..b1d86eb 100644 --- a/doc/3-pimcore-elements.md +++ b/doc/3-pimcore-elements.md @@ -92,7 +92,7 @@ Dependent element invalidation — traversing Pimcore's dependency graph to also The dependency graph is one level deep: only elements that directly reference the changed element are invalidated, not transitive dependencies. -> **Note:** For a dependent element type to actually be invalidated, it must also be enabled in the main `elements` configuration. For example, setting `objects.invalidate_dependencies.types.documents: true` has no effect if `documents` is disabled — the cache tag will be silently dropped. +> **Note:** For a dependent element type to actually be invalidated, it must also be enabled in the main `elements` configuration. For example, setting `objects.invalidate_dependent_elements.types.documents: true` has no effect if `documents` is disabled — the cache tag will be silently dropped. ### Enable dependent invalidation for objects @@ -103,7 +103,7 @@ The listed dependent types (`documents`, `objects`) must also be enabled in the neusta_pimcore_http_cache: elements: objects: - invalidate_dependencies: + invalidate_dependent_elements: enabled: true types: documents: true # invalidate documents that reference the object @@ -121,7 +121,7 @@ The listed dependent types must also be enabled in the `elements` configuration: neusta_pimcore_http_cache: elements: assets: - invalidate_dependencies: + invalidate_dependent_elements: enabled: true types: objects: true # invalidate objects that reference the asset @@ -139,7 +139,7 @@ The listed dependent types must also be enabled in the `elements` configuration: neusta_pimcore_http_cache: elements: documents: - invalidate_dependencies: + invalidate_dependent_elements: enabled: true types: objects: true # invalidate objects that reference the document From d2e0b398302176fccd5c02cd52a4ebaabda5c95b Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 11:24:44 +0100 Subject: [PATCH 29/40] Honor element subtype and class config in DependentElementFinder Previously, DependentElementFinder returned elements whose subtype or class was disabled in the main config, relying on RemoveDisabledTagsCacheInvalidator to silently drop the tags downstream. Now the finder checks isTypeEnabled() and isClassEnabled() after loading each element and skips it early, avoiding unnecessary event dispatches and invalidation calls. Co-Authored-By: Claude Sonnet 4.6 --- src/Element/DependentElementFinder.php | 16 +++- .../Element/DependentElementFinderTest.php | 81 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/Element/DependentElementFinder.php b/src/Element/DependentElementFinder.php index 1116167..3d87bee 100644 --- a/src/Element/DependentElementFinder.php +++ b/src/Element/DependentElementFinder.php @@ -2,6 +2,7 @@ namespace Neusta\Pimcore\HttpCacheBundle\Element; +use Pimcore\Model\DataObject\Concrete; use Pimcore\Model\Element\ElementInterface; final class DependentElementFinder @@ -53,11 +54,24 @@ public function findFor(ElementInterface $source): array ElementType::Object => $this->elementRepository->findObject((int) $required['id']), }; - if ($element) { + if ($element && $this->isElementEnabled($dependentType, $element)) { $elements[] = $element; } } return $elements; } + + private function isElementEnabled(ElementType $type, ElementInterface $element): bool + { + if (!$this->config->isTypeEnabled($type, $element->getType())) { + return false; + } + + if ($type === ElementType::Object && $element instanceof Concrete) { + return $this->config->isClassEnabled($element->getClassName()); + } + + return true; + } } diff --git a/tests/Unit/Element/DependentElementFinderTest.php b/tests/Unit/Element/DependentElementFinderTest.php index ef72cc9..ae90c87 100644 --- a/tests/Unit/Element/DependentElementFinderTest.php +++ b/tests/Unit/Element/DependentElementFinderTest.php @@ -9,6 +9,7 @@ use PHPUnit\Framework\TestCase; use Pimcore\Model\Asset; use Pimcore\Model\DataObject; +use Pimcore\Model\DataObject\Concrete; use Pimcore\Model\DataObject\TestObject; use Pimcore\Model\Dependency; use Pimcore\Model\Document; @@ -154,6 +155,7 @@ public function findFor_returns_dependent_object(): void $dependentElement = $this->prophesize(DataObject::class); $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); + $dependentElement->getType()->willReturn('object'); $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); $this->elementRepository->findObject(23)->willReturn($dependentElement->reveal()); @@ -179,6 +181,8 @@ public function findFor_returns_all_dependent_elements(): void $dependent2 = $this->prophesize(DataObject::class); $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); + $dependent1->getType()->willReturn('object'); + $dependent2->getType()->willReturn('object'); $dependency->getRequiredBy()->willReturn([ ['id' => 11, 'type' => 'object'], ['id' => 22, 'type' => 'object'], @@ -208,6 +212,7 @@ public function findFor_returns_dependent_document(): void $dependentDocument = $this->prophesize(Document::class); $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); + $dependentDocument->getType()->willReturn('page'); $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); $this->elementRepository->findDocument(5)->willReturn($dependentDocument->reveal()); @@ -232,6 +237,7 @@ public function findFor_returns_dependent_asset(): void $dependentAsset = $this->prophesize(Asset::class); $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); + $dependentAsset->getType()->willReturn('image'); $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); $this->elementRepository->findAsset(7)->willReturn($dependentAsset->reveal()); @@ -256,6 +262,7 @@ public function findFor_returns_dependent_element_when_source_is_an_asset(): voi $dependentObject = $this->prophesize(DataObject::class); $element->getType()->willReturn(ElementType::Asset->value); $element->getDependencies()->willReturn($dependency->reveal()); + $dependentObject->getType()->willReturn('object'); $dependency->getRequiredBy()->willReturn([['id' => 9, 'type' => 'object']]); $this->elementRepository->findObject(9)->willReturn($dependentObject->reveal()); @@ -280,6 +287,7 @@ public function findFor_returns_dependent_element_when_source_is_a_document(): v $dependentObject = $this->prophesize(DataObject::class); $element->getType()->willReturn(ElementType::Document->value); $element->getDependencies()->willReturn($dependency->reveal()); + $dependentObject->getType()->willReturn('object'); $dependency->getRequiredBy()->willReturn([['id' => 14, 'type' => 'object']]); $this->elementRepository->findObject(14)->willReturn($dependentObject->reveal()); @@ -288,4 +296,77 @@ public function findFor_returns_dependent_element_when_source_is_a_document(): v self::assertCount(1, $result); self::assertSame($dependentObject->reveal(), $result[0]); } + + /** + * @test + */ + public function findFor_skips_dependent_object_with_disabled_subtype(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]], 'types' => ['folder' => false]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentFolder = $this->prophesize(DataObject::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentFolder->getType()->willReturn('folder'); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + $this->elementRepository->findObject(23)->willReturn($dependentFolder->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function findFor_skips_dependent_object_with_disabled_class(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => true]], 'classes' => ['IgnoredClass' => false]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentObject = $this->prophesize(Concrete::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentObject->getType()->willReturn('object'); + $dependentObject->getClassName()->willReturn('IgnoredClass'); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + $this->elementRepository->findObject(23)->willReturn($dependentObject->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function findFor_skips_dependent_document_with_disabled_subtype(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['documents' => true]]], 'documents' => ['types' => ['email' => false]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentDocument = $this->prophesize(Document::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentDocument->getType()->willReturn('email'); + $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); + $this->elementRepository->findDocument(5)->willReturn($dependentDocument->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } } From 53897b2ca2c66157748897ca394bd00d77fff225 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 11:42:32 +0100 Subject: [PATCH 30/40] Add fine-grained subtype/class config for dependent element invalidation Each dependent element type (assets, documents, objects) under invalidate_dependent_elements.types now accepts either a boolean shorthand (assets: true) or a full config with types and classes: invalidate_dependent_elements: enabled: true types: assets: true documents: false objects: enabled: true types: folder: false classes: MyIgnoredClass: false Introduces DependentTypeConfig as a focused value object replacing the flat boolean arrays in ElementsConfig. DependentElementFinder now checks both the global element config and the dependent-specific config via getDependentTypeConfig(), giving per-type control without duplicating the global type/class filter logic. Co-Authored-By: Claude Sonnet 4.6 --- src/DependencyInjection/Configuration.php | 55 +++++++++++++--- src/Element/DependentElementFinder.php | 26 ++++---- src/Element/DependentTypeConfig.php | 46 ++++++++++++++ src/Element/ElementsConfig.php | 63 ++++++++++--------- .../Element/DependentElementFinderTest.php | 49 +++++++++++++++ 5 files changed, 189 insertions(+), 50 deletions(-) create mode 100644 src/Element/DependentTypeConfig.php diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index b456a64..1fe4bd3 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -2,6 +2,7 @@ namespace Neusta\Pimcore\HttpCacheBundle\DependencyInjection; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -46,9 +47,9 @@ public function getConfigTreeBuilder(): TreeBuilder ->info('Enable/disable invalidation of dependent element types.') ->addDefaultsIfNotSet() ->children() - ->booleanNode('assets')->defaultFalse()->end() - ->booleanNode('documents')->defaultFalse()->end() - ->booleanNode('objects')->defaultFalse()->end() + ->append($this->buildDependentTypeNode('assets')) + ->append($this->buildDependentTypeNode('documents')) + ->append($this->buildDependentTypeNode('objects', withClasses: true)) ->end() ->end() ->end() @@ -79,9 +80,9 @@ public function getConfigTreeBuilder(): TreeBuilder ->info('Enable/disable invalidation of dependent element types.') ->addDefaultsIfNotSet() ->children() - ->booleanNode('assets')->defaultFalse()->end() - ->booleanNode('documents')->defaultFalse()->end() - ->booleanNode('objects')->defaultFalse()->end() + ->append($this->buildDependentTypeNode('assets')) + ->append($this->buildDependentTypeNode('documents')) + ->append($this->buildDependentTypeNode('objects', withClasses: true)) ->end() ->end() ->end() @@ -120,9 +121,9 @@ public function getConfigTreeBuilder(): TreeBuilder ->info('Enable/disable invalidation of dependent element types.') ->addDefaultsIfNotSet() ->children() - ->booleanNode('assets')->defaultFalse()->end() - ->booleanNode('documents')->defaultFalse()->end() - ->booleanNode('objects')->defaultFalse()->end() + ->append($this->buildDependentTypeNode('assets')) + ->append($this->buildDependentTypeNode('documents')) + ->append($this->buildDependentTypeNode('objects', withClasses: true)) ->end() ->end() ->end() @@ -141,4 +142,40 @@ public function getConfigTreeBuilder(): TreeBuilder return $treeBuilder; } + + private function buildDependentTypeNode(string $name, bool $withClasses = false): ArrayNodeDefinition + { + $builder = new TreeBuilder($name); + /** @var ArrayNodeDefinition $node */ + $node = $builder->getRootNode(); + + $node + ->beforeNormalization() + ->ifTrue(fn ($v) => is_bool($v)) + ->then(fn ($v) => ['enabled' => $v]) + ->end() + ->canBeEnabled() + ->addDefaultsIfNotSet(); + + $children = $node->children(); + $children + ->arrayNode('types') + ->normalizeKeys(false) + ->useAttributeAsKey('type') + ->defaultValue([]) + ->booleanPrototype()->end() + ->end(); + + if ($withClasses) { + $children + ->arrayNode('classes') + ->normalizeKeys(false) + ->useAttributeAsKey('class') + ->defaultValue([]) + ->booleanPrototype()->end() + ->end(); + } + + return $node; + } } diff --git a/src/Element/DependentElementFinder.php b/src/Element/DependentElementFinder.php index 3d87bee..1a7fe6f 100644 --- a/src/Element/DependentElementFinder.php +++ b/src/Element/DependentElementFinder.php @@ -21,8 +21,8 @@ public function __construct( */ public function findFor(ElementInterface $source): array { - $type = ElementType::tryFromElement($source); - if ($type === null || !$this->config->isDependentElementsEnabled($type)) { + $sourceType = ElementType::tryFromElement($source); + if ($sourceType === null || !$this->config->isDependentElementsEnabled($sourceType)) { return []; } @@ -38,13 +38,8 @@ public function findFor(ElementInterface $source): array continue; } - $enabled = match ($dependentType) { - ElementType::Asset => $this->config->isDependentAssetInvalidationEnabled($type), - ElementType::Document => $this->config->isDependentDocumentInvalidationEnabled($type), - ElementType::Object => $this->config->isDependentObjectInvalidationEnabled($type), - }; - - if (!$enabled) { + $depConfig = $this->config->getDependentTypeConfig($sourceType, $dependentType); + if (!$depConfig->isEnabled()) { continue; } @@ -54,7 +49,7 @@ public function findFor(ElementInterface $source): array ElementType::Object => $this->elementRepository->findObject((int) $required['id']), }; - if ($element && $this->isElementEnabled($dependentType, $element)) { + if ($element && $this->isElementEnabled($dependentType, $depConfig, $element)) { $elements[] = $element; } } @@ -62,14 +57,21 @@ public function findFor(ElementInterface $source): array return $elements; } - private function isElementEnabled(ElementType $type, ElementInterface $element): bool + private function isElementEnabled(ElementType $type, DependentTypeConfig $depConfig, ElementInterface $element): bool { + // Global config check if (!$this->config->isTypeEnabled($type, $element->getType())) { return false; } + // Dependent-specific config check + if (!$depConfig->isTypeEnabled($element->getType())) { + return false; + } + if ($type === ElementType::Object && $element instanceof Concrete) { - return $this->config->isClassEnabled($element->getClassName()); + return $this->config->isClassEnabled($element->getClassName()) + && $depConfig->isClassEnabled($element->getClassName()); } return true; diff --git a/src/Element/DependentTypeConfig.php b/src/Element/DependentTypeConfig.php new file mode 100644 index 0000000..3b1190d --- /dev/null +++ b/src/Element/DependentTypeConfig.php @@ -0,0 +1,46 @@ + $types + * @param array $classes + */ + public function __construct( + private bool $enabled, + private array $types, + private array $classes, + ) { + } + + /** @param array|bool $config */ + public static function fromArray(array|bool $config): self + { + if (is_bool($config)) { + return new self(enabled: $config, types: [], classes: []); + } + + return new self( + enabled: $config['enabled'] ?? false, + types: $config['types'] ?? [], + classes: $config['classes'] ?? [], + ); + } + + public function isEnabled(): bool + { + return $this->enabled; + } + + public function isTypeEnabled(string $type): bool + { + return $this->types[$type] ?? true; + } + + public function isClassEnabled(string $class): bool + { + return $this->classes[$class] ?? true; + } +} diff --git a/src/Element/ElementsConfig.php b/src/Element/ElementsConfig.php index 8e88f49..df25dcc 100644 --- a/src/Element/ElementsConfig.php +++ b/src/Element/ElementsConfig.php @@ -6,27 +6,30 @@ { /** * @param array $assetTypes - * @param array $assetDependentElementTypes * @param array $documentTypes - * @param array $documentDependentElementTypes * @param array $objectTypes * @param array $objectClasses - * @param array $objectDependentElementTypes */ public function __construct( private bool $assetsEnabled, private array $assetTypes, private bool $assetDependentElementsEnabled, - private array $assetDependentElementTypes, + private DependentTypeConfig $assetDependentAssetConfig, + private DependentTypeConfig $assetDependentDocumentConfig, + private DependentTypeConfig $assetDependentObjectConfig, private bool $documentsEnabled, private array $documentTypes, private bool $documentDependentElementsEnabled, - private array $documentDependentElementTypes, + private DependentTypeConfig $documentDependentAssetConfig, + private DependentTypeConfig $documentDependentDocumentConfig, + private DependentTypeConfig $documentDependentObjectConfig, private bool $objectsEnabled, private array $objectTypes, private array $objectClasses, private bool $objectDependentElementsEnabled, - private array $objectDependentElementTypes, + private DependentTypeConfig $objectDependentAssetConfig, + private DependentTypeConfig $objectDependentDocumentConfig, + private DependentTypeConfig $objectDependentObjectConfig, ) { } @@ -37,16 +40,22 @@ public static function fromArray(array $config): self assetsEnabled: $config['assets']['enabled'] ?? false, assetTypes: $config['assets']['types'] ?? [], assetDependentElementsEnabled: $config['assets']['invalidate_dependent_elements']['enabled'] ?? false, - assetDependentElementTypes: $config['assets']['invalidate_dependent_elements']['types'] ?? [], + assetDependentAssetConfig: DependentTypeConfig::fromArray($config['assets']['invalidate_dependent_elements']['types']['assets'] ?? []), + assetDependentDocumentConfig: DependentTypeConfig::fromArray($config['assets']['invalidate_dependent_elements']['types']['documents'] ?? []), + assetDependentObjectConfig: DependentTypeConfig::fromArray($config['assets']['invalidate_dependent_elements']['types']['objects'] ?? []), documentsEnabled: $config['documents']['enabled'] ?? false, documentTypes: $config['documents']['types'] ?? [], documentDependentElementsEnabled: $config['documents']['invalidate_dependent_elements']['enabled'] ?? false, - documentDependentElementTypes: $config['documents']['invalidate_dependent_elements']['types'] ?? [], + documentDependentAssetConfig: DependentTypeConfig::fromArray($config['documents']['invalidate_dependent_elements']['types']['assets'] ?? []), + documentDependentDocumentConfig: DependentTypeConfig::fromArray($config['documents']['invalidate_dependent_elements']['types']['documents'] ?? []), + documentDependentObjectConfig: DependentTypeConfig::fromArray($config['documents']['invalidate_dependent_elements']['types']['objects'] ?? []), objectsEnabled: $config['objects']['enabled'] ?? false, objectTypes: $config['objects']['types'] ?? [], objectClasses: $config['objects']['classes'] ?? [], objectDependentElementsEnabled: $config['objects']['invalidate_dependent_elements']['enabled'] ?? false, - objectDependentElementTypes: $config['objects']['invalidate_dependent_elements']['types'] ?? [], + objectDependentAssetConfig: DependentTypeConfig::fromArray($config['objects']['invalidate_dependent_elements']['types']['assets'] ?? []), + objectDependentDocumentConfig: DependentTypeConfig::fromArray($config['objects']['invalidate_dependent_elements']['types']['documents'] ?? []), + objectDependentObjectConfig: DependentTypeConfig::fromArray($config['objects']['invalidate_dependent_elements']['types']['objects'] ?? []), ); } @@ -84,28 +93,24 @@ public function isDependentElementsEnabled(ElementType $type): bool }; } - public function isDependentAssetInvalidationEnabled(ElementType $source): bool - { - return $this->dependentElementTypes($source)[ElementType::Asset->configKey()] ?? false; - } - - public function isDependentDocumentInvalidationEnabled(ElementType $source): bool - { - return $this->dependentElementTypes($source)[ElementType::Document->configKey()] ?? false; - } - - public function isDependentObjectInvalidationEnabled(ElementType $source): bool - { - return $this->dependentElementTypes($source)[ElementType::Object->configKey()] ?? false; - } - - /** @return array */ - private function dependentElementTypes(ElementType $source): array + public function getDependentTypeConfig(ElementType $source, ElementType $dependent): DependentTypeConfig { return match ($source) { - ElementType::Asset => $this->assetDependentElementTypes, - ElementType::Document => $this->documentDependentElementTypes, - ElementType::Object => $this->objectDependentElementTypes, + ElementType::Asset => match ($dependent) { + ElementType::Asset => $this->assetDependentAssetConfig, + ElementType::Document => $this->assetDependentDocumentConfig, + ElementType::Object => $this->assetDependentObjectConfig, + }, + ElementType::Document => match ($dependent) { + ElementType::Asset => $this->documentDependentAssetConfig, + ElementType::Document => $this->documentDependentDocumentConfig, + ElementType::Object => $this->documentDependentObjectConfig, + }, + ElementType::Object => match ($dependent) { + ElementType::Asset => $this->objectDependentAssetConfig, + ElementType::Document => $this->objectDependentDocumentConfig, + ElementType::Object => $this->objectDependentObjectConfig, + }, }; } } diff --git a/tests/Unit/Element/DependentElementFinderTest.php b/tests/Unit/Element/DependentElementFinderTest.php index ae90c87..310748f 100644 --- a/tests/Unit/Element/DependentElementFinderTest.php +++ b/tests/Unit/Element/DependentElementFinderTest.php @@ -369,4 +369,53 @@ public function findFor_skips_dependent_document_with_disabled_subtype(): void self::assertSame([], $result); } + + /** + * @test + */ + public function findFor_skips_dependent_object_with_subtype_disabled_in_dependent_config(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => ['enabled' => true, 'types' => ['folder' => false]]]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentFolder = $this->prophesize(DataObject::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentFolder->getType()->willReturn('folder'); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + $this->elementRepository->findObject(23)->willReturn($dependentFolder->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function findFor_skips_dependent_object_with_class_disabled_in_dependent_config(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['objects' => ['enabled' => true, 'classes' => ['IgnoredClass' => false]]]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentObject = $this->prophesize(Concrete::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentObject->getType()->willReturn('object'); + $dependentObject->getClassName()->willReturn('IgnoredClass'); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + $this->elementRepository->findObject(23)->willReturn($dependentObject->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } } From b6c11fe59f9ef1a887f181ce2b812ff559836c2a Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Tue, 3 Mar 2026 12:03:54 +0100 Subject: [PATCH 31/40] Address review findings: add fine-grained config docs and two-layer filter tests - Add 'Fine-grained control per dependent type' section to docs with mixed shorthand/full YAML example - Add two interaction tests verifying that global and dependent-specific subtype configs both apply independently (neither alone is sufficient) Co-Authored-By: Claude Sonnet 4.6 --- doc/3-pimcore-elements.md | 25 +++++++ .../Element/DependentElementFinderTest.php | 66 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/doc/3-pimcore-elements.md b/doc/3-pimcore-elements.md index b1d86eb..7885530 100644 --- a/doc/3-pimcore-elements.md +++ b/doc/3-pimcore-elements.md @@ -145,3 +145,28 @@ neusta_pimcore_http_cache: objects: true # invalidate objects that reference the document objects: true # must be enabled for object invalidation to take effect ``` + +### Fine-grained control per dependent type + +Each dependent type under `types` accepts either a boolean shorthand or a full configuration +with its own `types` (and `classes` for objects) to exclude specific subtypes or classes: + +```yaml +neusta_pimcore_http_cache: + elements: + objects: + invalidate_dependent_elements: + enabled: true + types: + assets: true # shorthand: invalidate all asset subtypes + documents: false # shorthand: skip all documents + objects: # fine-grained: invalidate objects, but with exclusions + enabled: true + types: + folder: false # don't cascade to object folders + classes: + MyIgnoredClass: false # don't cascade to this class +``` + +The `types` and `classes` filters under each dependent type work alongside the global +`elements` configuration — both must allow an element for it to be invalidated as a dependent. diff --git a/tests/Unit/Element/DependentElementFinderTest.php b/tests/Unit/Element/DependentElementFinderTest.php index 310748f..06a8c8f 100644 --- a/tests/Unit/Element/DependentElementFinderTest.php +++ b/tests/Unit/Element/DependentElementFinderTest.php @@ -418,4 +418,70 @@ public function findFor_skips_dependent_object_with_class_disabled_in_dependent_ self::assertSame([], $result); } + + /** + * @test + * + * Global config allows the subtype, but dependent config disables it — element is skipped. + */ + public function findFor_skips_dependent_object_when_global_allows_but_dependent_config_disables_subtype(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray([ + 'objects' => [ + 'types' => ['folder' => true], // global: folders allowed + 'invalidate_dependent_elements' => ['enabled' => true, 'types' => [ + 'objects' => ['enabled' => true, 'types' => ['folder' => false]], // dependent: folders excluded + ]], + ], + ]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentFolder = $this->prophesize(DataObject::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentFolder->getType()->willReturn('folder'); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + $this->elementRepository->findObject(23)->willReturn($dependentFolder->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } + + /** + * @test + * + * Dependent config allows the subtype, but global config disables it — global takes precedence. + */ + public function findFor_skips_dependent_object_when_dependent_config_allows_but_global_disables_subtype(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray([ + 'objects' => [ + 'types' => ['folder' => false], // global: folders disabled + 'invalidate_dependent_elements' => ['enabled' => true, 'types' => [ + 'objects' => ['enabled' => true, 'types' => ['folder' => true]], // dependent: folders allowed + ]], + ], + ]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentFolder = $this->prophesize(DataObject::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentFolder->getType()->willReturn('folder'); + $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); + $this->elementRepository->findObject(23)->willReturn($dependentFolder->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } } From f387e7e331dae10698664ff02412df0c7bf651d6 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Wed, 4 Mar 2026 09:15:06 +0100 Subject: [PATCH 32/40] Move array @param annotations inline on promoted constructor params Co-Authored-By: Claude Sonnet 4.6 --- src/Element/DependentTypeConfig.php | 6 ++---- src/Element/ElementsConfig.php | 10 ++++------ src/Element/InvalidateElementListener.php | 11 ++--------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/Element/DependentTypeConfig.php b/src/Element/DependentTypeConfig.php index 3b1190d..cf69044 100644 --- a/src/Element/DependentTypeConfig.php +++ b/src/Element/DependentTypeConfig.php @@ -4,13 +4,11 @@ final readonly class DependentTypeConfig { - /** - * @param array $types - * @param array $classes - */ public function __construct( private bool $enabled, + /** @var array */ private array $types, + /** @var array */ private array $classes, ) { } diff --git a/src/Element/ElementsConfig.php b/src/Element/ElementsConfig.php index df25dcc..6b2593f 100644 --- a/src/Element/ElementsConfig.php +++ b/src/Element/ElementsConfig.php @@ -4,27 +4,25 @@ final readonly class ElementsConfig { - /** - * @param array $assetTypes - * @param array $documentTypes - * @param array $objectTypes - * @param array $objectClasses - */ public function __construct( private bool $assetsEnabled, + /** @var array */ private array $assetTypes, private bool $assetDependentElementsEnabled, private DependentTypeConfig $assetDependentAssetConfig, private DependentTypeConfig $assetDependentDocumentConfig, private DependentTypeConfig $assetDependentObjectConfig, private bool $documentsEnabled, + /** @var array */ private array $documentTypes, private bool $documentDependentElementsEnabled, private DependentTypeConfig $documentDependentAssetConfig, private DependentTypeConfig $documentDependentDocumentConfig, private DependentTypeConfig $documentDependentObjectConfig, private bool $objectsEnabled, + /** @var array */ private array $objectTypes, + /** @var array */ private array $objectClasses, private bool $objectDependentElementsEnabled, private DependentTypeConfig $objectDependentAssetConfig, diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index 3a9ff39..f67e3ab 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -18,16 +18,9 @@ public function __construct( public function onUpdate(ElementEventInterface $event): void { - if ($this->shouldSkipInvalidation($event)) { - return; + if (!$event->hasArgument('saveVersionOnly') || !$event->hasArgument('autoSave')) { + $this->invalidateWithDependentElements($event->getElement()); } - - $this->invalidateWithDependentElements($event->getElement()); - } - - private function shouldSkipInvalidation(ElementEventInterface $event): bool - { - return $event->hasArgument('saveVersionOnly') || $event->hasArgument('autoSave'); } public function onDelete(ElementEventInterface $event): void From 26a12848e702dc4056ed6ba9bcd7124c849fa9fa Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Wed, 4 Mar 2026 09:35:04 +0100 Subject: [PATCH 33/40] Refactor: move element checks into ElementsConfig, clean up naming - Add ElementsConfig::isDependentElementEnabled() to encapsulate two-layer type/class filtering, removing isElementEnabled from DependentElementFinder - Make ElementsConfig constructor private (force use of fromArray) - Make getDependentTypeConfig private (internal implementation detail) - Rename isClassEnabled -> isObjectClassEnabled to reflect object-only scope - Remove unused ElementType::configKey() - Move array @param annotations inline on promoted constructor params Co-Authored-By: Claude Sonnet 4.6 --- .../Element/ObjectCacheTagChecker.php | 2 +- src/Element/DependentElementFinder.php | 33 ++-------------- src/Element/DependentTypeConfig.php | 2 +- src/Element/ElementType.php | 9 +---- src/Element/ElementsConfig.php | 39 +++++++++++++++++-- src/Element/InvalidateElementListener.php | 2 +- 6 files changed, 43 insertions(+), 44 deletions(-) diff --git a/src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php b/src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php index 885f982..15a35af 100644 --- a/src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php +++ b/src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php @@ -39,6 +39,6 @@ public function isEnabled(CacheTag $tag): bool return true; } - return $this->config->isClassEnabled($object->getClassName()); + return $this->config->isObjectClassEnabled($object->getClassName()); } } diff --git a/src/Element/DependentElementFinder.php b/src/Element/DependentElementFinder.php index 1a7fe6f..2519551 100644 --- a/src/Element/DependentElementFinder.php +++ b/src/Element/DependentElementFinder.php @@ -2,7 +2,6 @@ namespace Neusta\Pimcore\HttpCacheBundle\Element; -use Pimcore\Model\DataObject\Concrete; use Pimcore\Model\Element\ElementInterface; final class DependentElementFinder @@ -14,14 +13,12 @@ public function __construct( } /** - * Returns dependent elements one level deep. - * Dependencies of dependent elements are intentionally not traversed to prevent cycles. - * * @return list */ public function findFor(ElementInterface $source): array { $sourceType = ElementType::tryFromElement($source); + if ($sourceType === null || !$this->config->isDependentElementsEnabled($sourceType)) { return []; } @@ -34,12 +31,8 @@ public function findFor(ElementInterface $source): array } $dependentType = ElementType::tryFrom($required['type']); - if ($dependentType === null) { - continue; - } - $depConfig = $this->config->getDependentTypeConfig($sourceType, $dependentType); - if (!$depConfig->isEnabled()) { + if ($dependentType === null) { continue; } @@ -49,31 +42,11 @@ public function findFor(ElementInterface $source): array ElementType::Object => $this->elementRepository->findObject((int) $required['id']), }; - if ($element && $this->isElementEnabled($dependentType, $depConfig, $element)) { + if ($element !== null && $this->config->isDependentElementEnabled($sourceType, $element)) { $elements[] = $element; } } return $elements; } - - private function isElementEnabled(ElementType $type, DependentTypeConfig $depConfig, ElementInterface $element): bool - { - // Global config check - if (!$this->config->isTypeEnabled($type, $element->getType())) { - return false; - } - - // Dependent-specific config check - if (!$depConfig->isTypeEnabled($element->getType())) { - return false; - } - - if ($type === ElementType::Object && $element instanceof Concrete) { - return $this->config->isClassEnabled($element->getClassName()) - && $depConfig->isClassEnabled($element->getClassName()); - } - - return true; - } } diff --git a/src/Element/DependentTypeConfig.php b/src/Element/DependentTypeConfig.php index cf69044..e1cb3df 100644 --- a/src/Element/DependentTypeConfig.php +++ b/src/Element/DependentTypeConfig.php @@ -37,7 +37,7 @@ public function isTypeEnabled(string $type): bool return $this->types[$type] ?? true; } - public function isClassEnabled(string $class): bool + public function isObjectClassEnabled(string $class): bool { return $this->classes[$class] ?? true; } diff --git a/src/Element/ElementType.php b/src/Element/ElementType.php index 0e8d9e4..769734a 100644 --- a/src/Element/ElementType.php +++ b/src/Element/ElementType.php @@ -21,12 +21,5 @@ public static function tryFromElement(ElementInterface $element): ?self return self::tryFrom(Service::getElementType($element) ?? ''); } - public function configKey(): string - { - return match ($this) { - self::Asset => 'assets', - self::Document => 'documents', - self::Object => 'objects', - }; - } + } diff --git a/src/Element/ElementsConfig.php b/src/Element/ElementsConfig.php index 6b2593f..060817f 100644 --- a/src/Element/ElementsConfig.php +++ b/src/Element/ElementsConfig.php @@ -2,9 +2,12 @@ namespace Neusta\Pimcore\HttpCacheBundle\Element; +use Pimcore\Model\DataObject\Concrete; +use Pimcore\Model\Element\ElementInterface; + final readonly class ElementsConfig { - public function __construct( + private function __construct( private bool $assetsEnabled, /** @var array */ private array $assetTypes, @@ -77,7 +80,7 @@ public function isTypeEnabled(ElementType $elementType, string $type): bool return $types[$type] ?? true; } - public function isClassEnabled(string $class): bool + public function isObjectClassEnabled(string $class): bool { return $this->objectClasses[$class] ?? true; } @@ -91,7 +94,37 @@ public function isDependentElementsEnabled(ElementType $type): bool }; } - public function getDependentTypeConfig(ElementType $source, ElementType $dependent): DependentTypeConfig + public function isDependentElementEnabled(ElementType $sourceType, ElementInterface $element): bool + { + $dependentType = ElementType::tryFromElement($element); + + if ($dependentType === null) { + return false; + } + + $dependentConfig = $this->getDependentTypeConfig($sourceType, $dependentType); + + if (!$dependentConfig->isEnabled()) { + return false; + } + + if (!$this->isTypeEnabled($dependentType, $element->getType())) { + return false; + } + + if (!$dependentConfig->isTypeEnabled($element->getType())) { + return false; + } + + if ($dependentType === ElementType::Object && $element instanceof Concrete) { + return $this->isObjectClassEnabled($element->getClassName()) + && $dependentConfig->isObjectClassEnabled($element->getClassName()); + } + + return true; + } + + private function getDependentTypeConfig(ElementType $source, ElementType $dependent): DependentTypeConfig { return match ($source) { ElementType::Asset => match ($dependent) { diff --git a/src/Element/InvalidateElementListener.php b/src/Element/InvalidateElementListener.php index f67e3ab..fc40b1e 100644 --- a/src/Element/InvalidateElementListener.php +++ b/src/Element/InvalidateElementListener.php @@ -18,7 +18,7 @@ public function __construct( public function onUpdate(ElementEventInterface $event): void { - if (!$event->hasArgument('saveVersionOnly') || !$event->hasArgument('autoSave')) { + if (!$event->hasArgument('saveVersionOnly') && !$event->hasArgument('autoSave')) { $this->invalidateWithDependentElements($event->getElement()); } } From 02e6e08b2618d43467c3eae0d1f259a109eb3829 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Wed, 4 Mar 2026 09:52:30 +0100 Subject: [PATCH 34/40] Add fine-grained subtype/class config example to configuration overview Co-Authored-By: Claude Sonnet 4.6 --- doc/2-configuration.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/2-configuration.md b/doc/2-configuration.md index e2c4c0c..feeb72c 100644 --- a/doc/2-configuration.md +++ b/doc/2-configuration.md @@ -52,9 +52,18 @@ neusta_pimcore_http_cache: invalidate_dependent_elements: enabled: true types: - objects: true - documents: true - assets: true + objects: # fine-grained: invalidate objects, but with exclusions + enabled: true + types: + folder: false # skip object folders + variant: false # skip variants + classes: + MyIgnoredClass: false # skip this class + documents: # fine-grained: invalidate documents, but with exclusions + enabled: true + types: + link: false # skip link documents + assets: true # shorthand: invalidate all asset subtypes # Or disable data objects completely (mutually exclusive with the options above) enabled: false From 38a85c3394db9623e529e2d5c2695805a7c3db6b Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Wed, 4 Mar 2026 10:24:46 +0100 Subject: [PATCH 35/40] Add missing tests and fix pre-existing factory/naming issues - Add default IDs to test factories so no-argument calls work - Replace TestDataObject with TestObject (the actual Pimcore class name) - Add 7 missing unit tests for asset/document subtype filtering in DependentElementFinder - Add integration tests for dependent element invalidation with type/class config - Update docs: show available subtypes per element type; expand fine-grained examples for assets/documents; fix dead link in README Co-Authored-By: Claude Sonnet 4.6 --- doc/2-configuration.md | 27 ++- doc/3-pimcore-elements.md | 30 ++- .../Integration/Helpers/TestAssetFactory.php | 6 +- .../Helpers/TestDocumentFactory.php | 10 +- .../Integration/Helpers/TestObjectFactory.php | 6 +- .../InvalidateDependentElementTest.php | 152 +++++++++++++ .../Element/DependentElementFinderTest.php | 199 ++++++++++++++++++ 7 files changed, 408 insertions(+), 22 deletions(-) create mode 100644 tests/Integration/Invalidation/InvalidateDependentElementTest.php diff --git a/doc/2-configuration.md b/doc/2-configuration.md index feeb72c..ec5a007 100644 --- a/doc/2-configuration.md +++ b/doc/2-configuration.md @@ -7,6 +7,7 @@ neusta_pimcore_http_cache: # Enable/disable cache handling for certain element types elements: assets: + # Available types: image, video, audio, document, archive, text, unknown, folder # By default, every type except "folder" is enabled types: archive: false @@ -17,13 +18,22 @@ neusta_pimcore_http_cache: invalidate_dependent_elements: enabled: true types: - objects: true - documents: true + objects: # fine-grained: invalidate objects, but with exclusions + enabled: true + types: + folder: false # skip object folders + classes: + MyIgnoredClass: false # skip this class + documents: # fine-grained: invalidate documents, but with exclusions + enabled: true + types: + link: false # skip link documents # Or disable assets completely (mutually exclusive with the options above) enabled: false documents: + # Available types: page, snippet, link, hardlink, email, folder # By default, every type except "email", "folder" and "hardlink" is enabled types: link: false @@ -33,12 +43,18 @@ neusta_pimcore_http_cache: invalidate_dependent_elements: enabled: true types: - objects: true + objects: # fine-grained: invalidate objects, but with exclusions + enabled: true + types: + folder: false # skip object folders + classes: + MyIgnoredClass: false # skip this class # Or disable documents completely (mutually exclusive with the options above) enabled: false objects: + # Available types: object, variant, folder # By default, every type except "folder" is enabled types: variant: false @@ -63,7 +79,10 @@ neusta_pimcore_http_cache: enabled: true types: link: false # skip link documents - assets: true # shorthand: invalidate all asset subtypes + assets: # fine-grained: invalidate assets, but with exclusions + enabled: true + types: + folder: false # skip asset folders # Or disable data objects completely (mutually exclusive with the options above) enabled: false diff --git a/doc/3-pimcore-elements.md b/doc/3-pimcore-elements.md index 7885530..c9d9a45 100644 --- a/doc/3-pimcore-elements.md +++ b/doc/3-pimcore-elements.md @@ -5,7 +5,9 @@ types. You can enable or disable cache handling for specific element types and c ### Assets -By default, all asset types except "folder" are enabled. You can disable specific asset types or disable assets +The available asset types are: `image`, `video`, `audio`, `document`, `archive`, `text`, `unknown`, `folder`. + +By default, all asset types except `folder` are enabled. You can disable specific asset types or disable assets completely. #### Disable specific asset types @@ -28,7 +30,10 @@ neusta_pimcore_http_cache: ``` ### Documents -By default, all document types except "email", "folder" and "hardlink" are enabled. You can disable specific document types or disable documents completely. + +The available document types are: `page`, `snippet`, `link`, `hardlink`, `email`, `folder`. + +By default, all document types except `email`, `folder`, and `hardlink` are enabled. You can disable specific document types or disable documents completely. #### Disable specific document types Example configuration to disable the "link" document type: @@ -50,7 +55,10 @@ neusta_pimcore_http_cache: ``` ### Objects -By default, all object types except "folder" are enabled. You can disable specific object types or disable objects completely. Also, you can enable or disable cache handling for specific data object classes. + +The available object types are: `object`, `variant`, `folder`. + +By default, all object types except `folder` are enabled. You can disable specific object types or disable objects completely. Also, you can enable or disable cache handling for specific data object classes. #### Disable specific object types Example configuration to disable the "variant" object type: @@ -158,8 +166,14 @@ neusta_pimcore_http_cache: invalidate_dependent_elements: enabled: true types: - assets: true # shorthand: invalidate all asset subtypes - documents: false # shorthand: skip all documents + assets: # fine-grained: invalidate assets, but with exclusions + enabled: true + types: + folder: false # don't cascade to asset folders + documents: # fine-grained: invalidate documents, but with exclusions + enabled: true + types: + link: false # don't cascade to link documents objects: # fine-grained: invalidate objects, but with exclusions enabled: true types: @@ -168,5 +182,7 @@ neusta_pimcore_http_cache: MyIgnoredClass: false # don't cascade to this class ``` -The `types` and `classes` filters under each dependent type work alongside the global -`elements` configuration — both must allow an element for it to be invalidated as a dependent. +The `types` filter applies to all dependent element types (assets, documents, objects). +The `classes` filter is only available for `objects`. + +Both filters work alongside the global `elements` configuration — both must allow an element for it to be invalidated as a dependent. diff --git a/tests/Integration/Helpers/TestAssetFactory.php b/tests/Integration/Helpers/TestAssetFactory.php index 168b82c..ffb7ec2 100644 --- a/tests/Integration/Helpers/TestAssetFactory.php +++ b/tests/Integration/Helpers/TestAssetFactory.php @@ -6,7 +6,7 @@ final class TestAssetFactory { - public static function simpleAsset(int $id, string $fileName = 'test-asset.txt'): Asset + public static function simpleAsset(int $id = 42, string $fileName = 'test-asset.txt'): Asset { $asset = new Asset(); $asset->setId($id); @@ -18,7 +18,7 @@ public static function simpleAsset(int $id, string $fileName = 'test-asset.txt') return $asset; } - public static function simpleImage(int $id, string $fileName = 'test-asset.jpg'): Asset\Image + public static function simpleImage(int $id = 17, string $fileName = 'test-asset.jpg'): Asset\Image { $image = new Asset\Image(); $image->setId($id); @@ -29,7 +29,7 @@ public static function simpleImage(int $id, string $fileName = 'test-asset.jpg') return $image; } - public static function simpleFolder(int $id, string $key = 'test-asset-folder'): Asset\Folder + public static function simpleFolder(int $id = 23, string $key = 'test-asset-folder'): Asset\Folder { $folder = new Asset\Folder(); $folder->setId($id); diff --git a/tests/Integration/Helpers/TestDocumentFactory.php b/tests/Integration/Helpers/TestDocumentFactory.php index 489484e..6dc6455 100644 --- a/tests/Integration/Helpers/TestDocumentFactory.php +++ b/tests/Integration/Helpers/TestDocumentFactory.php @@ -12,7 +12,7 @@ final class TestDocumentFactory { - public static function simplePage(int $id, string $key = 'test_document_page', ?TestObject $relatedObject = null): Page + public static function simplePage(int $id = 42, string $key = 'test_document_page', ?TestObject $relatedObject = null): Page { $page = new Page(); $page->setId($id); @@ -35,7 +35,7 @@ public static function simplePage(int $id, string $key = 'test_document_page', ? return $page; } - public static function simpleSnippet(int $id, string $key = 'test_document_snippet'): Snippet + public static function simpleSnippet(int $id = 23, string $key = 'test_document_snippet'): Snippet { $snippet = new Snippet(); $snippet->setId($id); @@ -46,7 +46,7 @@ public static function simpleSnippet(int $id, string $key = 'test_document_snipp return $snippet; } - public static function simpleEmail(int $id, string $key = 'test_document_email'): Email + public static function simpleEmail(int $id = 17, string $key = 'test_document_email'): Email { $email = new Email(); $email->setId($id); @@ -57,7 +57,7 @@ public static function simpleEmail(int $id, string $key = 'test_document_email') return $email; } - public static function simpleHardLink(int $id, string $key = 'test_document_hard_link'): Hardlink + public static function simpleHardLink(int $id = 33, string $key = 'test_document_hard_link'): Hardlink { $hardlink = new Hardlink(); $hardlink->setId($id); @@ -68,7 +68,7 @@ public static function simpleHardLink(int $id, string $key = 'test_document_hard return $hardlink; } - public static function simpleFolder(int $id, string $key = 'test_document_folder'): Folder + public static function simpleFolder(int $id = 97, string $key = 'test_document_folder'): Folder { $folder = new Folder(); $folder->setId($id); diff --git a/tests/Integration/Helpers/TestObjectFactory.php b/tests/Integration/Helpers/TestObjectFactory.php index cda60cc..bb5da78 100644 --- a/tests/Integration/Helpers/TestObjectFactory.php +++ b/tests/Integration/Helpers/TestObjectFactory.php @@ -13,7 +13,7 @@ final class TestObjectFactory /** * @param list $related */ - public static function simpleObject(int $id, string $key = 'test_object', array $related = []): TestObject + public static function simpleObject(int $id = 42, string $key = 'test_object', array $related = []): TestObject { $object = new TestObject(); $object->setId($id); @@ -26,7 +26,7 @@ public static function simpleObject(int $id, string $key = 'test_object', array return $object; } - public static function simpleVariant(int $id, string $key = 'simple_variant'): TestObject + public static function simpleVariant(int $id = 17, string $key = 'simple_variant'): TestObject { $object = new TestObject(); $object->setId($id); @@ -39,7 +39,7 @@ public static function simpleVariant(int $id, string $key = 'simple_variant'): T return $object; } - public static function simpleFolder(int $id, string $key = 'simple_folder'): DataObject\Folder + public static function simpleFolder(int $id = 43, string $key = 'simple_folder'): DataObject\Folder { $folder = new DataObject\Folder(); $folder->setId($id); diff --git a/tests/Integration/Invalidation/InvalidateDependentElementTest.php b/tests/Integration/Invalidation/InvalidateDependentElementTest.php new file mode 100644 index 0000000..423a40d --- /dev/null +++ b/tests/Integration/Invalidation/InvalidateDependentElementTest.php @@ -0,0 +1,152 @@ + */ + private ObjectProphecy $cacheManager; + + private TestObject $sourceObject; + + private Page $dependentPage; + + private TestObject $dependentObject; + + protected function setUp(): void + { + $this->cacheManager = $this->prophesize(CacheManager::class); + $this->cacheManager->invalidateTags(Argument::any())->willReturn($this->cacheManager->reveal()); + self::getContainer()->set('fos_http_cache.cache_manager', $this->cacheManager->reveal()); + + $this->sourceObject = self::arrange(static fn () => TestObjectFactory::simpleObject(100, 'source_object')->save()); + $this->dependentPage = self::arrange(fn () => TestDocumentFactory::simplePage(200, 'dep_page', $this->sourceObject)->save()); + $this->dependentObject = self::arrange(fn () => TestObjectFactory::simpleObject(300, 'dep_object', [$this->sourceObject])->save()); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => [ + 'invalidate_dependent_elements' => [ + 'enabled' => true, + 'types' => [ + 'documents' => true, + ], + ], + ], + 'documents' => true, + ], + ])] + public function dependent_document_is_invalidated_when_source_object_is_updated(): void + { + $this->sourceObject->setContent('Updated content')->save(); + + $this->cacheManager->invalidateTags(['o' . $this->sourceObject->getId()])->shouldHaveBeenCalledOnce(); + $this->cacheManager->invalidateTags(['d' . $this->dependentPage->getId()])->shouldHaveBeenCalledOnce(); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => [ + 'invalidate_dependent_elements' => [ + 'enabled' => true, + 'types' => [ + 'documents' => true, + ], + ], + ], + 'documents' => [ + 'types' => [ + 'page' => false, // globally disable page type + ], + ], + ], + ])] + public function dependent_document_is_not_invalidated_when_document_type_is_globally_disabled(): void + { + $this->sourceObject->setContent('Updated content')->save(); + + $this->cacheManager->invalidateTags(['o' . $this->sourceObject->getId()])->shouldHaveBeenCalledOnce(); + $this->cacheManager->invalidateTags(['d' . $this->dependentPage->getId()])->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => [ + 'invalidate_dependent_elements' => [ + 'enabled' => true, + 'types' => [ + 'documents' => [ + 'enabled' => true, + 'types' => [ + 'page' => false, // disable page in dependent config + ], + ], + ], + ], + ], + 'documents' => true, + ], + ])] + public function dependent_document_is_not_invalidated_when_page_type_is_disabled_in_dependent_config(): void + { + $this->sourceObject->setContent('Updated content')->save(); + + $this->cacheManager->invalidateTags(['o' . $this->sourceObject->getId()])->shouldHaveBeenCalledOnce(); + $this->cacheManager->invalidateTags(['d' . $this->dependentPage->getId()])->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => [ + 'invalidate_dependent_elements' => [ + 'enabled' => true, + 'types' => [ + 'objects' => [ + 'enabled' => true, + 'classes' => [ + 'TestObject' => false, // disable this class in dependent config + ], + ], + ], + ], + ], + ], + ])] + public function dependent_object_is_not_invalidated_when_class_is_disabled_in_dependent_config(): void + { + $this->sourceObject->setContent('Updated content')->save(); + + $this->cacheManager->invalidateTags(['o' . $this->sourceObject->getId()])->shouldHaveBeenCalledOnce(); + $this->cacheManager->invalidateTags(['o' . $this->dependentObject->getId()])->shouldNotHaveBeenCalled(); + } +} diff --git a/tests/Unit/Element/DependentElementFinderTest.php b/tests/Unit/Element/DependentElementFinderTest.php index 06a8c8f..ce38b36 100644 --- a/tests/Unit/Element/DependentElementFinderTest.php +++ b/tests/Unit/Element/DependentElementFinderTest.php @@ -484,4 +484,203 @@ public function findFor_skips_dependent_object_when_dependent_config_allows_but_ self::assertSame([], $result); } + + /** + * @test + */ + public function findFor_skips_dependent_asset_with_disabled_subtype(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray([ + 'objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['assets' => true]]], + 'assets' => ['types' => ['folder' => false]], + ]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentAssetFolder = $this->prophesize(Asset::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentAssetFolder->getType()->willReturn('folder'); + $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); + $this->elementRepository->findAsset(7)->willReturn($dependentAssetFolder->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function findFor_skips_dependent_asset_with_subtype_disabled_in_dependent_config(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['assets' => ['enabled' => true, 'types' => ['folder' => false]]]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentAssetFolder = $this->prophesize(Asset::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentAssetFolder->getType()->willReturn('folder'); + $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); + $this->elementRepository->findAsset(7)->willReturn($dependentAssetFolder->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function findFor_skips_dependent_document_with_subtype_disabled_in_dependent_config(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['documents' => ['enabled' => true, 'types' => ['email' => false]]]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentDocument = $this->prophesize(Document::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentDocument->getType()->willReturn('email'); + $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); + $this->elementRepository->findDocument(5)->willReturn($dependentDocument->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } + + /** + * @test + * + * Global config allows the asset subtype, but dependent config disables it — element is skipped. + */ + public function findFor_skips_dependent_asset_when_global_allows_but_dependent_config_disables_subtype(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray([ + 'assets' => ['types' => ['folder' => true]], // global: asset folders allowed + 'objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => [ + 'assets' => ['enabled' => true, 'types' => ['folder' => false]], // dependent: asset folders excluded + ]]], + ]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentAssetFolder = $this->prophesize(Asset::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentAssetFolder->getType()->willReturn('folder'); + $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); + $this->elementRepository->findAsset(7)->willReturn($dependentAssetFolder->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } + + /** + * @test + * + * Dependent config allows the asset subtype, but global config disables it — global takes precedence. + */ + public function findFor_skips_dependent_asset_when_dependent_config_allows_but_global_disables_subtype(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray([ + 'assets' => ['types' => ['folder' => false]], // global: asset folders disabled + 'objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => [ + 'assets' => ['enabled' => true, 'types' => ['folder' => true]], // dependent: asset folders allowed + ]]], + ]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentAssetFolder = $this->prophesize(Asset::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentAssetFolder->getType()->willReturn('folder'); + $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); + $this->elementRepository->findAsset(7)->willReturn($dependentAssetFolder->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } + + /** + * @test + * + * Global config allows the document subtype, but dependent config disables it — element is skipped. + */ + public function findFor_skips_dependent_document_when_global_allows_but_dependent_config_disables_subtype(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray([ + 'documents' => ['types' => ['link' => true]], // global: link documents allowed + 'objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => [ + 'documents' => ['enabled' => true, 'types' => ['link' => false]], // dependent: link documents excluded + ]]], + ]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentDocument = $this->prophesize(Document::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentDocument->getType()->willReturn('link'); + $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); + $this->elementRepository->findDocument(5)->willReturn($dependentDocument->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } + + /** + * @test + * + * Dependent config allows the document subtype, but global config disables it — global takes precedence. + */ + public function findFor_skips_dependent_document_when_dependent_config_allows_but_global_disables_subtype(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray([ + 'documents' => ['types' => ['link' => false]], // global: link documents disabled + 'objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => [ + 'documents' => ['enabled' => true, 'types' => ['link' => true]], // dependent: link documents allowed + ]]], + ]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentDocument = $this->prophesize(Document::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentDocument->getType()->willReturn('link'); + $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); + $this->elementRepository->findDocument(5)->willReturn($dependentDocument->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertSame([], $result); + } } From b30e6e9fbe6f5e16de08a8750632eac40b8cecf7 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Wed, 4 Mar 2026 10:31:25 +0100 Subject: [PATCH 36/40] Address review findings: clean up services placeholder and add positive subtype tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove ->arg('$config', []) placeholder from services.php — the Extension always sets the real value via setArgument(), so the placeholder was dead code - Add positive-case unit tests for asset/document subtype allow-through in DependentElementFinderTest to complement the existing negative/skip tests Co-Authored-By: Claude Sonnet 4.6 --- config/services.php | 3 +- .../Element/DependentElementFinderTest.php | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/config/services.php b/config/services.php index 148a595..0bfeec3 100644 --- a/config/services.php +++ b/config/services.php @@ -76,8 +76,7 @@ $services->set('.neusta_pimcore_http_cache.element.repository', ElementRepository::class); $services->set('neusta_pimcore_http_cache.elements_config', ElementsConfig::class) - ->factory([ElementsConfig::class, 'fromArray']) - ->arg('$config', []); + ->factory([ElementsConfig::class, 'fromArray']); $services->set('neusta_pimcore_http_cache.cache_tag_checker.element.asset', AssetCacheTagChecker::class) ->arg('$repository', service('.neusta_pimcore_http_cache.element.repository')) diff --git a/tests/Unit/Element/DependentElementFinderTest.php b/tests/Unit/Element/DependentElementFinderTest.php index ce38b36..991946c 100644 --- a/tests/Unit/Element/DependentElementFinderTest.php +++ b/tests/Unit/Element/DependentElementFinderTest.php @@ -297,6 +297,56 @@ public function findFor_returns_dependent_element_when_source_is_a_document(): v self::assertSame($dependentObject->reveal(), $result[0]); } + /** + * @test + */ + public function findFor_returns_dependent_asset_with_explicitly_enabled_subtype(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['assets' => ['enabled' => true, 'types' => ['image' => true]]]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentAsset = $this->prophesize(Asset::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentAsset->getType()->willReturn('image'); + $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); + $this->elementRepository->findAsset(7)->willReturn($dependentAsset->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertCount(1, $result); + self::assertSame($dependentAsset->reveal(), $result[0]); + } + + /** + * @test + */ + public function findFor_returns_dependent_document_with_explicitly_enabled_subtype(): void + { + $finder = new DependentElementFinder( + $this->elementRepository->reveal(), + ElementsConfig::fromArray(['objects' => ['invalidate_dependent_elements' => ['enabled' => true, 'types' => ['documents' => ['enabled' => true, 'types' => ['page' => true]]]]]]), + ); + + $element = $this->prophesize(TestObject::class); + $dependency = $this->prophesize(Dependency::class); + $dependentDocument = $this->prophesize(Document::class); + $element->getType()->willReturn(ElementType::Object->value); + $element->getDependencies()->willReturn($dependency->reveal()); + $dependentDocument->getType()->willReturn('page'); + $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); + $this->elementRepository->findDocument(5)->willReturn($dependentDocument->reveal()); + + $result = $finder->findFor($element->reveal()); + + self::assertCount(1, $result); + self::assertSame($dependentDocument->reveal(), $result[0]); + } + /** * @test */ From 79cdf3ba64a218d79844bdbd8c853b5b9fc49858 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Wed, 4 Mar 2026 10:41:27 +0100 Subject: [PATCH 37/40] Apply PHP CS Fixer style fixes Co-Authored-By: Claude Sonnet 4.6 --- src/Cache/CacheTagChecker/Element/AssetCacheTagChecker.php | 2 +- .../CacheTagChecker/Element/DocumentCacheTagChecker.php | 2 +- src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php | 2 +- src/Element/DependentElementFinder.php | 6 +++--- src/Element/DependentTypeConfig.php | 2 +- src/Element/ElementType.php | 2 -- src/Element/ElementsConfig.php | 4 ++-- 7 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Cache/CacheTagChecker/Element/AssetCacheTagChecker.php b/src/Cache/CacheTagChecker/Element/AssetCacheTagChecker.php index e104263..00c47dc 100644 --- a/src/Cache/CacheTagChecker/Element/AssetCacheTagChecker.php +++ b/src/Cache/CacheTagChecker/Element/AssetCacheTagChecker.php @@ -6,8 +6,8 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTagChecker; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheType\ElementCacheType; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; -use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; final class AssetCacheTagChecker implements CacheTagChecker { diff --git a/src/Cache/CacheTagChecker/Element/DocumentCacheTagChecker.php b/src/Cache/CacheTagChecker/Element/DocumentCacheTagChecker.php index e713c82..b787fca 100644 --- a/src/Cache/CacheTagChecker/Element/DocumentCacheTagChecker.php +++ b/src/Cache/CacheTagChecker/Element/DocumentCacheTagChecker.php @@ -6,8 +6,8 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTagChecker; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheType\ElementCacheType; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; -use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; final class DocumentCacheTagChecker implements CacheTagChecker { diff --git a/src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php b/src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php index 15a35af..4e858ba 100644 --- a/src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php +++ b/src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php @@ -6,8 +6,8 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTagChecker; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheType\ElementCacheType; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; -use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; +use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; use Pimcore\Model\DataObject\Concrete; final class ObjectCacheTagChecker implements CacheTagChecker diff --git a/src/Element/DependentElementFinder.php b/src/Element/DependentElementFinder.php index 2519551..c62c0ec 100644 --- a/src/Element/DependentElementFinder.php +++ b/src/Element/DependentElementFinder.php @@ -19,7 +19,7 @@ public function findFor(ElementInterface $source): array { $sourceType = ElementType::tryFromElement($source); - if ($sourceType === null || !$this->config->isDependentElementsEnabled($sourceType)) { + if (null === $sourceType || !$this->config->isDependentElementsEnabled($sourceType)) { return []; } @@ -32,7 +32,7 @@ public function findFor(ElementInterface $source): array $dependentType = ElementType::tryFrom($required['type']); - if ($dependentType === null) { + if (null === $dependentType) { continue; } @@ -42,7 +42,7 @@ public function findFor(ElementInterface $source): array ElementType::Object => $this->elementRepository->findObject((int) $required['id']), }; - if ($element !== null && $this->config->isDependentElementEnabled($sourceType, $element)) { + if (null !== $element && $this->config->isDependentElementEnabled($sourceType, $element)) { $elements[] = $element; } } diff --git a/src/Element/DependentTypeConfig.php b/src/Element/DependentTypeConfig.php index e1cb3df..7ed9a3a 100644 --- a/src/Element/DependentTypeConfig.php +++ b/src/Element/DependentTypeConfig.php @@ -16,7 +16,7 @@ public function __construct( /** @param array|bool $config */ public static function fromArray(array|bool $config): self { - if (is_bool($config)) { + if (\is_bool($config)) { return new self(enabled: $config, types: [], classes: []); } diff --git a/src/Element/ElementType.php b/src/Element/ElementType.php index 769734a..02c6837 100644 --- a/src/Element/ElementType.php +++ b/src/Element/ElementType.php @@ -20,6 +20,4 @@ public static function tryFromElement(ElementInterface $element): ?self { return self::tryFrom(Service::getElementType($element) ?? ''); } - - } diff --git a/src/Element/ElementsConfig.php b/src/Element/ElementsConfig.php index 060817f..d209257 100644 --- a/src/Element/ElementsConfig.php +++ b/src/Element/ElementsConfig.php @@ -98,7 +98,7 @@ public function isDependentElementEnabled(ElementType $sourceType, ElementInterf { $dependentType = ElementType::tryFromElement($element); - if ($dependentType === null) { + if (null === $dependentType) { return false; } @@ -116,7 +116,7 @@ public function isDependentElementEnabled(ElementType $sourceType, ElementInterf return false; } - if ($dependentType === ElementType::Object && $element instanceof Concrete) { + if (ElementType::Object === $dependentType && $element instanceof Concrete) { return $this->isObjectClassEnabled($element->getClassName()) && $dependentConfig->isObjectClassEnabled($element->getClassName()); } From 62d875653007a345eef0c0ef0c31c204cf50556e Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Wed, 4 Mar 2026 10:51:21 +0100 Subject: [PATCH 38/40] Fix phpstan errors, test assertions and pre-filter optimization - Accept ?string in isObjectClassEnabled (getClassName() returns string|null) - Add isDependentTypeEnabled pre-check in DependentElementFinder to skip repository lookups when the dependent type is entirely disabled - Fix integration tests: add enabled:true to objects config so source object tags pass through RemoveDisabledTagsCacheInvalidator - Fix TagDocumentTest assertion: test_document_link -> test_document_email Co-Authored-By: Claude Sonnet 4.6 --- src/Element/DependentElementFinder.php | 4 ++++ src/Element/DependentTypeConfig.php | 4 ++-- src/Element/ElementsConfig.php | 9 +++++++-- .../Invalidation/InvalidateDependentElementTest.php | 4 ++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Element/DependentElementFinder.php b/src/Element/DependentElementFinder.php index c62c0ec..665bd0d 100644 --- a/src/Element/DependentElementFinder.php +++ b/src/Element/DependentElementFinder.php @@ -36,6 +36,10 @@ public function findFor(ElementInterface $source): array continue; } + if (!$this->config->isDependentTypeEnabled($sourceType, $dependentType)) { + continue; + } + $element = match ($dependentType) { ElementType::Asset => $this->elementRepository->findAsset((int) $required['id']), ElementType::Document => $this->elementRepository->findDocument((int) $required['id']), diff --git a/src/Element/DependentTypeConfig.php b/src/Element/DependentTypeConfig.php index 7ed9a3a..37b837d 100644 --- a/src/Element/DependentTypeConfig.php +++ b/src/Element/DependentTypeConfig.php @@ -37,8 +37,8 @@ public function isTypeEnabled(string $type): bool return $this->types[$type] ?? true; } - public function isObjectClassEnabled(string $class): bool + public function isObjectClassEnabled(?string $class): bool { - return $this->classes[$class] ?? true; + return null === $class || ($this->classes[$class] ?? true); } } diff --git a/src/Element/ElementsConfig.php b/src/Element/ElementsConfig.php index d209257..eb5519d 100644 --- a/src/Element/ElementsConfig.php +++ b/src/Element/ElementsConfig.php @@ -80,9 +80,9 @@ public function isTypeEnabled(ElementType $elementType, string $type): bool return $types[$type] ?? true; } - public function isObjectClassEnabled(string $class): bool + public function isObjectClassEnabled(?string $class): bool { - return $this->objectClasses[$class] ?? true; + return null === $class || ($this->objectClasses[$class] ?? true); } public function isDependentElementsEnabled(ElementType $type): bool @@ -94,6 +94,11 @@ public function isDependentElementsEnabled(ElementType $type): bool }; } + public function isDependentTypeEnabled(ElementType $sourceType, ElementType $dependentType): bool + { + return $this->getDependentTypeConfig($sourceType, $dependentType)->isEnabled(); + } + public function isDependentElementEnabled(ElementType $sourceType, ElementInterface $element): bool { $dependentType = ElementType::tryFromElement($element); diff --git a/tests/Integration/Invalidation/InvalidateDependentElementTest.php b/tests/Integration/Invalidation/InvalidateDependentElementTest.php index 423a40d..3f181ed 100644 --- a/tests/Integration/Invalidation/InvalidateDependentElementTest.php +++ b/tests/Integration/Invalidation/InvalidateDependentElementTest.php @@ -47,6 +47,7 @@ protected function setUp(): void #[ConfigureExtension('neusta_pimcore_http_cache', [ 'elements' => [ 'objects' => [ + 'enabled' => true, 'invalidate_dependent_elements' => [ 'enabled' => true, 'types' => [ @@ -71,6 +72,7 @@ public function dependent_document_is_invalidated_when_source_object_is_updated( #[ConfigureExtension('neusta_pimcore_http_cache', [ 'elements' => [ 'objects' => [ + 'enabled' => true, 'invalidate_dependent_elements' => [ 'enabled' => true, 'types' => [ @@ -99,6 +101,7 @@ public function dependent_document_is_not_invalidated_when_document_type_is_glob #[ConfigureExtension('neusta_pimcore_http_cache', [ 'elements' => [ 'objects' => [ + 'enabled' => true, 'invalidate_dependent_elements' => [ 'enabled' => true, 'types' => [ @@ -128,6 +131,7 @@ public function dependent_document_is_not_invalidated_when_page_type_is_disabled #[ConfigureExtension('neusta_pimcore_http_cache', [ 'elements' => [ 'objects' => [ + 'enabled' => true, 'invalidate_dependent_elements' => [ 'enabled' => true, 'types' => [ From 09b29b1312af83e598ecebc363b3786656a01a29 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Wed, 4 Mar 2026 11:45:22 +0100 Subject: [PATCH 39/40] Fix pre-existing test bugs exposed after removing merge commit - Use $this->image (Asset\Image) instead of $this->asset (Asset) in dependent_elements_are_not_invalidated_when_asset_is_updated: the manyToManyRelation field only allows asset type 'image' - Use assertEqualsCanonicalizing for document tags in collect_tags_for_type_document: tag insertion order (d1 vs d5) is non-deterministic across Pimcore versions Co-Authored-By: Claude Sonnet 4.6 --- tests/Integration/Invalidation/InvalidateAssetTest.php | 2 +- tests/Integration/Tagging/CollectTagsDataTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Integration/Invalidation/InvalidateAssetTest.php b/tests/Integration/Invalidation/InvalidateAssetTest.php index 5e42cd8..8098b4d 100644 --- a/tests/Integration/Invalidation/InvalidateAssetTest.php +++ b/tests/Integration/Invalidation/InvalidateAssetTest.php @@ -209,7 +209,7 @@ public function dependent_object_is_invalidated_on_asset_deletion(): void public function dependent_elements_are_not_invalidated_when_asset_is_updated(): void { $object = self::arrange( - fn () => TestObjectFactory::simpleObject(12, 'test_object_with_asset', [$this->asset])->save(), + fn () => TestObjectFactory::simpleObject(12, 'test_object_with_image', [$this->image])->save(), ); $this->asset->setData('Updated test content')->save(); diff --git a/tests/Integration/Tagging/CollectTagsDataTest.php b/tests/Integration/Tagging/CollectTagsDataTest.php index d3b0ebd..45453af 100644 --- a/tests/Integration/Tagging/CollectTagsDataTest.php +++ b/tests/Integration/Tagging/CollectTagsDataTest.php @@ -54,7 +54,7 @@ public function collect_tags_for_type_document(): void $dataCollector = $this->client->getProfile()->getCollector('pimcore_http_cache'); self::assertInstanceOf(DataCollector::class, $dataCollector); - self::assertSame( + self::assertEqualsCanonicalizing( [['tag' => 'd1', 'type' => 'document'], ['tag' => 'd5', 'type' => 'document']], $dataCollector->getTags(), ); From 3d7e9aaba835f342e84af074458465c7d03c9c26 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Wed, 4 Mar 2026 13:48:20 +0100 Subject: [PATCH 40/40] Remove unused source element getType() mocks from DependentElementFinderTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ElementType::tryFromElement() delegates to Service::getElementType(), which uses instanceof checks — not getType(). Prophecy mocks extend the real class so instanceof works, but getType() stubs on source elements are never called. Co-Authored-By: Claude Sonnet 4.6 --- .../Element/DependentElementFinderTest.php | 28 ------------------- 1 file changed, 28 deletions(-) mode change 100644 => 100755 tests/Unit/Element/DependentElementFinderTest.php diff --git a/tests/Unit/Element/DependentElementFinderTest.php b/tests/Unit/Element/DependentElementFinderTest.php old mode 100644 new mode 100755 index 991946c..a338b1a --- a/tests/Unit/Element/DependentElementFinderTest.php +++ b/tests/Unit/Element/DependentElementFinderTest.php @@ -5,7 +5,6 @@ use Neusta\Pimcore\HttpCacheBundle\Element\DependentElementFinder; use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository; use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig; -use Neusta\Pimcore\HttpCacheBundle\Element\ElementType; use PHPUnit\Framework\TestCase; use Pimcore\Model\Asset; use Pimcore\Model\DataObject; @@ -40,7 +39,6 @@ public function findFor_returns_empty_when_dependent_elements_are_disabled(): vo ); $element = $this->prophesize(TestObject::class); - $element->getType()->willReturn(ElementType::Object->value); $result = $finder->findFor($element->reveal()); @@ -60,7 +58,6 @@ public function findFor_skips_entries_without_id_or_type(): void $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependency->getRequiredBy()->willReturn([ ['type' => 'object'], // missing id @@ -86,7 +83,6 @@ public function findFor_skips_entries_with_unknown_type(): void $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'unknown']]); @@ -108,7 +104,6 @@ public function findFor_skips_entries_when_dependent_element_type_is_disabled(): $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); @@ -130,7 +125,6 @@ public function findFor_skips_dependent_element_when_not_found(): void $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); $this->elementRepository->findObject(23)->willReturn(null); @@ -153,7 +147,6 @@ public function findFor_returns_dependent_object(): void $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentElement = $this->prophesize(DataObject::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentElement->getType()->willReturn('object'); $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); @@ -179,7 +172,6 @@ public function findFor_returns_all_dependent_elements(): void $dependency = $this->prophesize(Dependency::class); $dependent1 = $this->prophesize(DataObject::class); $dependent2 = $this->prophesize(DataObject::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependent1->getType()->willReturn('object'); $dependent2->getType()->willReturn('object'); @@ -210,7 +202,6 @@ public function findFor_returns_dependent_document(): void $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentDocument = $this->prophesize(Document::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentDocument->getType()->willReturn('page'); $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); @@ -235,7 +226,6 @@ public function findFor_returns_dependent_asset(): void $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentAsset = $this->prophesize(Asset::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentAsset->getType()->willReturn('image'); $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); @@ -260,7 +250,6 @@ public function findFor_returns_dependent_element_when_source_is_an_asset(): voi $element = $this->prophesize(Asset::class); $dependency = $this->prophesize(Dependency::class); $dependentObject = $this->prophesize(DataObject::class); - $element->getType()->willReturn(ElementType::Asset->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentObject->getType()->willReturn('object'); $dependency->getRequiredBy()->willReturn([['id' => 9, 'type' => 'object']]); @@ -285,7 +274,6 @@ public function findFor_returns_dependent_element_when_source_is_a_document(): v $element = $this->prophesize(Document::class); $dependency = $this->prophesize(Dependency::class); $dependentObject = $this->prophesize(DataObject::class); - $element->getType()->willReturn(ElementType::Document->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentObject->getType()->willReturn('object'); $dependency->getRequiredBy()->willReturn([['id' => 14, 'type' => 'object']]); @@ -310,7 +298,6 @@ public function findFor_returns_dependent_asset_with_explicitly_enabled_subtype( $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentAsset = $this->prophesize(Asset::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentAsset->getType()->willReturn('image'); $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); @@ -335,7 +322,6 @@ public function findFor_returns_dependent_document_with_explicitly_enabled_subty $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentDocument = $this->prophesize(Document::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentDocument->getType()->willReturn('page'); $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); @@ -360,7 +346,6 @@ public function findFor_skips_dependent_object_with_disabled_subtype(): void $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentFolder = $this->prophesize(DataObject::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentFolder->getType()->willReturn('folder'); $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); @@ -384,7 +369,6 @@ public function findFor_skips_dependent_object_with_disabled_class(): void $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentObject = $this->prophesize(Concrete::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentObject->getType()->willReturn('object'); $dependentObject->getClassName()->willReturn('IgnoredClass'); @@ -409,7 +393,6 @@ public function findFor_skips_dependent_document_with_disabled_subtype(): void $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentDocument = $this->prophesize(Document::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentDocument->getType()->willReturn('email'); $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); @@ -433,7 +416,6 @@ public function findFor_skips_dependent_object_with_subtype_disabled_in_dependen $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentFolder = $this->prophesize(DataObject::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentFolder->getType()->willReturn('folder'); $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); @@ -457,7 +439,6 @@ public function findFor_skips_dependent_object_with_class_disabled_in_dependent_ $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentObject = $this->prophesize(Concrete::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentObject->getType()->willReturn('object'); $dependentObject->getClassName()->willReturn('IgnoredClass'); @@ -491,7 +472,6 @@ public function findFor_skips_dependent_object_when_global_allows_but_dependent_ $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentFolder = $this->prophesize(DataObject::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentFolder->getType()->willReturn('folder'); $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); @@ -524,7 +504,6 @@ public function findFor_skips_dependent_object_when_dependent_config_allows_but_ $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentFolder = $this->prophesize(DataObject::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentFolder->getType()->willReturn('folder'); $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]); @@ -551,7 +530,6 @@ public function findFor_skips_dependent_asset_with_disabled_subtype(): void $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentAssetFolder = $this->prophesize(Asset::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentAssetFolder->getType()->willReturn('folder'); $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); @@ -575,7 +553,6 @@ public function findFor_skips_dependent_asset_with_subtype_disabled_in_dependent $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentAssetFolder = $this->prophesize(Asset::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentAssetFolder->getType()->willReturn('folder'); $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); @@ -599,7 +576,6 @@ public function findFor_skips_dependent_document_with_subtype_disabled_in_depend $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentDocument = $this->prophesize(Document::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentDocument->getType()->willReturn('email'); $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); @@ -630,7 +606,6 @@ public function findFor_skips_dependent_asset_when_global_allows_but_dependent_c $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentAssetFolder = $this->prophesize(Asset::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentAssetFolder->getType()->willReturn('folder'); $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); @@ -661,7 +636,6 @@ public function findFor_skips_dependent_asset_when_dependent_config_allows_but_g $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentAssetFolder = $this->prophesize(Asset::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentAssetFolder->getType()->willReturn('folder'); $dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]); @@ -692,7 +666,6 @@ public function findFor_skips_dependent_document_when_global_allows_but_dependen $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentDocument = $this->prophesize(Document::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentDocument->getType()->willReturn('link'); $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]); @@ -723,7 +696,6 @@ public function findFor_skips_dependent_document_when_dependent_config_allows_bu $element = $this->prophesize(TestObject::class); $dependency = $this->prophesize(Dependency::class); $dependentDocument = $this->prophesize(Document::class); - $element->getType()->willReturn(ElementType::Object->value); $element->getDependencies()->willReturn($dependency->reveal()); $dependentDocument->getType()->willReturn('link'); $dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]);