Skip to content

Invalidate dependent elements#37

Draft
jan888adams wants to merge 40 commits intomainfrom
invalidate-dependent-elements
Draft

Invalidate dependent elements#37
jan888adams wants to merge 40 commits intomainfrom
invalidate-dependent-elements

Conversation

@jan888adams
Copy link
Copy Markdown
Contributor

@jan888adams jan888adams commented Aug 7, 2025

Summary by CodeRabbit

  • New Features

    • Added dependent element invalidation: one-level-deep invalidation of Pimcore elements (assets, documents, objects) that reference changed elements.
  • Documentation

    • Added new "Dependent Element Invalidation" section explaining opt-in configuration and behavior.
    • Updated configuration documentation with invalidate_dependencies options for assets, documents, and objects.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 7, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 96fe3355-569a-43ea-8e3e-b9c77f15a192

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch invalidate-dependent-elements

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jan888adams jan888adams force-pushed the invalidate-dependent-elements branch from 9dfee3b to cfd6107 Compare August 12, 2025 12:54
Base automatically changed from display-tags-in-symfony-profiler to main August 13, 2025 10:58
@jan888adams jan888adams force-pushed the invalidate-dependent-elements branch from 7fc57dd to 6aedd15 Compare August 20, 2025 10:16

$this->invalidateElement($element);

if (ElementType::Object === ElementType::tryFrom($element->getType())) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only for objects?

Copy link
Copy Markdown
Contributor Author

@jan888adams jan888adams Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wouldn’t make sense in every case.
If an object is required by a document, why should all sites that use this object be invalidated?
If a snippet is required by a page, why should all pages containing that snippet be invalidated?
I think we need to carefully consider in which cases this behavior actually makes sense.

Copy link
Copy Markdown
Member

@jdreesen jdreesen Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wouldn’t make sense in every case.

You don't know that. Only the app will know, right?

If an object is required by a document, why should all sites that use this object be invalidated?

Probably because it is somehow used in the document, so when it's not invalidated it displays outdated data?

If a snippet is required by a page, why should all pages containing that snippet be invalidated?

Same reason: why should the page require the snippet if it doesn't use it? And if it does, and it isn't invalidated, it shows outdated data.

I think we need to carefully consider in which cases this behavior actually makes sense.

Here I'm with you. Maybe we need an (easy) mechanism that controls whether the dependencies should be invalidated or not (also: which ones?) 🤔

Copy link
Copy Markdown
Contributor Author

@jan888adams jan888adams Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you are right. The examples were not selected carefully.

Indeed, it should be: a Document is required by an Object. That may or may not make sense, but it’s a case the user of the bundle could have control over.

Also, a Page being required by a Snippet doesn’t sound like a standard case. If we want to do that, it should be considered more carefully.

So the idea was: cases where an Object is required by another Object or by a Document are clear. The other cases are not standard and should not be included in our default behavior.

The best way to handle them is, as you said, through configuration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is now possible to flexibly configure all element types. Disabled by default.

@jan888adams jan888adams force-pushed the invalidate-dependent-elements branch 2 times, most recently from ea6878b to af41501 Compare August 21, 2025 11:17
$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']),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check if invalidating an asset makes sense.

Copy link
Copy Markdown
Contributor Author

@jan888adams jan888adams Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We kept assets in the dependency traversal, though honestly we're not 100% certain this will ever be needed in practice.

Here's the reasoning: standard Pimcore asset metadata supports an object field type, which means an asset can hold a reference to a data object. When Pimcore saves that asset, it records
the relation as a dependency — so object.getDependencies()->getRequiredBy() can return assets. Whether anyone actually uses this in the wild, we don't know. But the infrastructure supports
it, so ignoring assets felt like an arbitrary gap.

What makes invalidating an asset's cache tag useful here is the cascade effect. When a page renders and loads an asset, this bundle tags that page response with the asset's tag (e.g. a42).
So purging a42 doesn't just clear the asset's own cached file response — it also clears every page that loaded the asset. If those pages render information from the asset's object
metadata (a caption, a credit, a classification), they'd otherwise serve stale content after the object changes.

We'll revisit this later. The plan is to make the dependency traversal configurable in the bundle config, so projects can opt in or out per element type rather than having it hardcoded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, assets can be enabled if needed in config

@jan888adams jan888adams force-pushed the invalidate-dependent-elements branch from 38d1ff8 to d58cf4f Compare March 2, 2026 07:39
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php (1)

74-76: Consider reusing the existing $invalidateListener variable.

The definition is already retrieved on line 36. You could reuse it instead of fetching again:

♻️ Suggested simplification
-        $container->getDefinition('neusta_pimcore_http_cache.element.invalidate_listener')
-            ->setArgument('$config', $config);
+        $invalidateListener
+            ->setArgument('$config', $config);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php` around lines 74
- 76, The code fetches the
'neusta_pimcore_http_cache.element.invalidate_listener' definition twice;
instead reuse the previously retrieved $invalidateListener variable (from where
it's defined earlier) and call ->setArgument('$config', $config) on
$invalidateListener instead of calling $container->getDefinition(...) again to
avoid redundant retrieval and improve clarity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Element/InvalidateElementListener.php`:
- Around line 36-43: After calling invalidateElement in
invalidateWithDependencies, ensure you do not traverse/invalidate dependents if
the source invalidation was canceled: update invalidateWithDependencies to check
the element's cancellation state (e.g. a boolean like isInvalidationCanceled()
or a return value from invalidateElement) and only call
isDependencyTraversalEnabled and invalidateDependencies when the invalidation
was not canceled; use the existing methods invalidateElement,
isDependencyTraversalEnabled, and invalidateDependencies to locate where to add
this guard.
- Around line 40-43: The code currently calls
ElementType::tryFrom($element->getType()), which fails for subtype strings like
"image" or "page" and causes dependency traversal in
isDependencyTraversalEnabled/ invalidateDependencies to be skipped; replace the
tryFrom(getType()) call with ElementType::fromElement($element) (which uses
Pimcore\Model\Element\Service::getElementType()) so $type resolves to the
correct generic type before invoking isDependencyTraversalEnabled($type) and
invalidateDependencies($element->getDependencies(), $type).

In `@tests/Integration/Tagging/CollectTagsDataTest.php`:
- Line 49: The test calls TestDocumentFactory::simplePage(5)->save() after
arrange() returns, which causes the save to run with caching active; move the
save() call inside the closure passed to self::arrange so the document is
created and saved during arrange. Update the call in CollectTagsDataTest to
self::arrange(fn () => TestDocumentFactory::simplePage(5)->save()) so that
TestDocumentFactory::simplePage and its save() are executed within arrange().

In `@tests/Integration/Tagging/TagAdditionalTagTest.php`:
- Around line 107-124: The assertion in the test method
response_is_tagged_with_additional_tag_when_object_is_loaded (class
TagAdditionalTagTest) is checking the primary object's tag 'o5' instead of the
additional tag added by the ElementTaggingEvent listener; update the final
assertion that inspects the X-Cache-Tags header to expect 'o12' (the additional
tag produced by CacheTag::fromString('12', new
ElementCacheType(ElementType::Object))) instead of 'o5' so the test aligns with
the method intent and the asset/document variants.

In `@tests/Integration/Tagging/TagDocumentTest.php`:
- Around line 103-112: The assertion checks for tag 'd29' but the test arranges
a hardlink with id 12 via TestDocumentFactory::simpleHardLink(12)->save() and
then requests '/get-document?id=12', so update the assertion to check for the
correct tag for id 12 (i.e. assert that X-Cache-Tags does not contain 'd12' or
adjust to the expected tag string for that hardlink) by modifying the final
assertion that inspects $response->headers->get('X-Cache-Tags') to reference the
hardlink id used in TestDocumentFactory::simpleHardLink.

---

Nitpick comments:
In `@src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php`:
- Around line 74-76: The code fetches the
'neusta_pimcore_http_cache.element.invalidate_listener' definition twice;
instead reuse the previously retrieved $invalidateListener variable (from where
it's defined earlier) and call ->setArgument('$config', $config) on
$invalidateListener instead of calling $container->getDefinition(...) again to
avoid redundant retrieval and improve clarity.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26c75fd and d58cf4f.

📒 Files selected for processing (23)
  • config/services.php
  • doc/2-configuration.md
  • doc/3-pimcore-elements.md
  • src/DependencyInjection/Configuration.php
  • src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php
  • src/Element/InvalidateElementListener.php
  • tests/Integration/Configuration/CollectConfigurationDataTest.php
  • tests/Integration/Helpers/TestAssetFactory.php
  • tests/Integration/Helpers/TestDocumentFactory.php
  • tests/Integration/Helpers/TestObjectFactory.php
  • tests/Integration/Invalidation/CancelInvalidationTest.php
  • tests/Integration/Invalidation/InvalidateAdditionalTagTest.php
  • tests/Integration/Invalidation/InvalidateAssetTest.php
  • tests/Integration/Invalidation/InvalidateDocumentTest.php
  • tests/Integration/Invalidation/InvalidateObjectTest.php
  • tests/Integration/Tagging/CollectTagsDataTest.php
  • tests/Integration/Tagging/TagAdditionalTagTest.php
  • tests/Integration/Tagging/TagAssetTest.php
  • tests/Integration/Tagging/TagDocumentTest.php
  • tests/Integration/Tagging/TagObjectTest.php
  • tests/Unit/Element/InvalidateElementListenerTest.php
  • tests/app/config/pimcore/classes/definition_TestObject.php
  • tests/app/src/Controller/GetObjectController.php

Comment thread src/Element/InvalidateElementListener.php Outdated
Comment thread src/Element/InvalidateElementListener.php Outdated
Comment thread tests/Integration/Tagging/CollectTagsDataTest.php Outdated
Comment thread tests/Integration/Tagging/TagAdditionalTagTest.php Outdated
Comment thread tests/Integration/Tagging/TagDocumentTest.php Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/Element/InvalidateElementListener.php (1)

67-90: Consider batching invalidation for high fan-out dependencies.

For elements with many requiredBy entries, per-element invalidation calls may become expensive. If this becomes a hotspot, aggregating tags and issuing fewer invalidator calls would improve throughput.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Element/InvalidateElementListener.php` around lines 67 - 90, The
invalidateDependencies method currently calls invalidateElement for each
dependent in a loop which can be expensive for high fan-out; refactor
invalidateDependencies (and possibly add a helper like
collectAndInvalidateBatch) to aggregate elements/tags per dependent ElementType
(use ElementType::tryFrom, configKey($dependentType), and
elementRepository->findObject/findDocument/findAsset) into batches and then call
the invalidator once per batch instead of per element (or use an
invalidateElements(array $elements) / invalidateTags(array $tags) variant if
available) so fewer invalidator calls are issued while preserving the existing
type filters and the final call to invalidateElement for remaining singletons.
src/DependencyInjection/Configuration.php (1)

40-55: Consider extracting the duplicated invalidate_dependencies subtree builder.

The three blocks are functionally aligned, but copy/paste here is easy to drift over time. A small private helper to append this subtree would improve maintainability.

Also applies to: 73-88, 114-129

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/DependencyInjection/Configuration.php` around lines 40 - 55, The
invalidate_dependencies subtree is duplicated in multiple places; extract its
construction into a private helper method (e.g., private function
appendInvalidateDependencies(NodeDefinition $node) or private function
addInvalidateDependencies(ArrayNodeDefinition $parent)) that encapsulates the
->arrayNode('invalidate_dependencies')->info(...)->canBeEnabled()->addDefaultsIfNotSet()->children()->arrayNode('types')->info(...)->addDefaultsIfNotSet()->children()->booleanNode('assets')->defaultFalse()->end()->booleanNode('documents')->defaultFalse()->end()->booleanNode('objects')->defaultFalse()->end()->end()->end()->end()
logic, then replace the three inline copies with calls to this helper from the
Configuration builder (referencing the existing
arrayNode('invalidate_dependencies') occurrences). Ensure the helper returns or
mutates the passed node consistently and add a small unit/test if present to
verify the subtree is still added.
tests/Unit/Element/InvalidateElementListenerTest.php (1)

191-208: Use list-shaped dependency fixtures to mirror runtime payloads.

getRequiredBy() is mocked as a single associative array in this provider. The listener iterates over a list of dependency records, so these fixtures should use list form to avoid false confidence in future changes.

♻️ Suggested fixture shape adjustment
-        $dependency->getRequiredBy()->willReturn(['id' => 23, 'type' => 'object']);
+        $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]);
@@
-        $dependency->getRequiredBy()->willReturn(['id' => 23, 'type' => 'object']);
+        $dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Unit/Element/InvalidateElementListenerTest.php` around lines 191 - 208,
The provider notObjectElementProvider() uses Dependency::getRequiredBy() to
return a single associative array, but the listener expects a list of dependency
records; update both Asset and Document fixtures so Dependency::getRequiredBy()
returns a list-shaped value (i.e., an array/iterable whose elements are
associative records like ['id' => 23, 'type' => 'object']) so the tests mirror
the runtime payload processed by AssetEvent and DocumentEvent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/DependencyInjection/Configuration.php`:
- Around line 40-55: The invalidate_dependencies subtree is duplicated in
multiple places; extract its construction into a private helper method (e.g.,
private function appendInvalidateDependencies(NodeDefinition $node) or private
function addInvalidateDependencies(ArrayNodeDefinition $parent)) that
encapsulates the
->arrayNode('invalidate_dependencies')->info(...)->canBeEnabled()->addDefaultsIfNotSet()->children()->arrayNode('types')->info(...)->addDefaultsIfNotSet()->children()->booleanNode('assets')->defaultFalse()->end()->booleanNode('documents')->defaultFalse()->end()->booleanNode('objects')->defaultFalse()->end()->end()->end()->end()
logic, then replace the three inline copies with calls to this helper from the
Configuration builder (referencing the existing
arrayNode('invalidate_dependencies') occurrences). Ensure the helper returns or
mutates the passed node consistently and add a small unit/test if present to
verify the subtree is still added.

In `@src/Element/InvalidateElementListener.php`:
- Around line 67-90: The invalidateDependencies method currently calls
invalidateElement for each dependent in a loop which can be expensive for high
fan-out; refactor invalidateDependencies (and possibly add a helper like
collectAndInvalidateBatch) to aggregate elements/tags per dependent ElementType
(use ElementType::tryFrom, configKey($dependentType), and
elementRepository->findObject/findDocument/findAsset) into batches and then call
the invalidator once per batch instead of per element (or use an
invalidateElements(array $elements) / invalidateTags(array $tags) variant if
available) so fewer invalidator calls are issued while preserving the existing
type filters and the final call to invalidateElement for remaining singletons.

In `@tests/Unit/Element/InvalidateElementListenerTest.php`:
- Around line 191-208: The provider notObjectElementProvider() uses
Dependency::getRequiredBy() to return a single associative array, but the
listener expects a list of dependency records; update both Asset and Document
fixtures so Dependency::getRequiredBy() returns a list-shaped value (i.e., an
array/iterable whose elements are associative records like ['id' => 23, 'type'
=> 'object']) so the tests mirror the runtime payload processed by AssetEvent
and DocumentEvent.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26c75fd and dd73cf9.

📒 Files selected for processing (24)
  • config/services.php
  • doc/2-configuration.md
  • doc/3-pimcore-elements.md
  • src/DependencyInjection/Configuration.php
  • src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php
  • src/Element/ElementType.php
  • src/Element/InvalidateElementListener.php
  • tests/Integration/Configuration/CollectConfigurationDataTest.php
  • tests/Integration/Helpers/TestAssetFactory.php
  • tests/Integration/Helpers/TestDocumentFactory.php
  • tests/Integration/Helpers/TestObjectFactory.php
  • tests/Integration/Invalidation/CancelInvalidationTest.php
  • tests/Integration/Invalidation/InvalidateAdditionalTagTest.php
  • tests/Integration/Invalidation/InvalidateAssetTest.php
  • tests/Integration/Invalidation/InvalidateDocumentTest.php
  • tests/Integration/Invalidation/InvalidateObjectTest.php
  • tests/Integration/Tagging/CollectTagsDataTest.php
  • tests/Integration/Tagging/TagAdditionalTagTest.php
  • tests/Integration/Tagging/TagAssetTest.php
  • tests/Integration/Tagging/TagDocumentTest.php
  • tests/Integration/Tagging/TagObjectTest.php
  • tests/Unit/Element/InvalidateElementListenerTest.php
  • tests/app/config/pimcore/classes/definition_TestObject.php
  • tests/app/src/Controller/GetObjectController.php

Comment thread doc/2-configuration.md Outdated
# Unless you disable data objects completely
# 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:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think element types alone might not be sufficient. We should probably consider subtypes as well.

jan888adams and others added 18 commits March 3, 2026 09:53
…ests

- 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 <noreply@anthropic.com>
…cking)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nce objects

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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Jan Adams and others added 6 commits March 3, 2026 09:53
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ases

- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
@jan888adams jan888adams force-pushed the invalidate-dependent-elements branch from 9ffba3d to 00f70ab Compare March 3, 2026 08:53
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/DependencyInjection/Configuration.php (1)

40-55: Reduce duplication in invalidate_dependencies subtree definitions.

The same subtree is repeated three times (assets/documents/objects). Extracting a small helper would reduce drift risk and future maintenance overhead.

♻️ Refactor sketch
+use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
+
+private function addInvalidateDependenciesNode(ArrayNodeDefinition $node, string $subject): void
+{
+    $node
+        ->arrayNode('invalidate_dependencies')
+            ->info(sprintf('Enable/disable invalidation of dependent elements when %s is updated or deleted.', $subject))
+            ->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();
+}

Also applies to: 73-88, 114-129

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/DependencyInjection/Configuration.php` around lines 40 - 55, The
invalidate_dependencies subtree repeats identical boolean child node definitions
for 'assets', 'documents', and 'objects' under the 'types' ArrayNode; refactor
by extracting a small helper method (e.g., addTypeBooleanNodes or
addBooleanChildrenToTypes) that takes the parent ArrayNodeDefinition (the
'types' node) and an array of names and adds the
booleanNode(...)->defaultFalse()->end() entries, then replace each repeated
block (the three occurrences around invalidate_dependencies/types) with calls to
that helper to reduce duplication and future drift.
doc/2-configuration.md (1)

15-58: Clarify opt-in intent in the YAML example to avoid accidental enablement.

The comments say “disabled by default,” but the example sets all invalidate_dependencies.enabled flags to true. In a full config snippet, this can be misread as recommended default config.

Suggested docs tweak
-            invalidate_dependencies:
-                enabled: true
+            invalidate_dependencies:
+                enabled: true # opt-in example; default is false
                 types:
                     objects: true
                     documents: true
@@
-            invalidate_dependencies:
-                enabled: true
+            invalidate_dependencies:
+                enabled: true # opt-in example; default is false
                 types:
                     objects: true
@@
-            invalidate_dependencies:
-                enabled: true
+            invalidate_dependencies:
+                enabled: true # opt-in example; default is false
                 types:
                     objects: true
                     documents: true
                     assets: true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@doc/2-configuration.md` around lines 15 - 58, The YAML example currently
shows invalidate_dependencies.enabled set to true for assets, documents and
objects which contradicts the “disabled by default” comment; update the snippet
so each invalidate_dependencies.enabled is set to false (or clearly mark the
shown true values as an explicit opt-in example) and add a short inline comment
next to invalidate_dependencies.enabled (e.g., "# default: false — opt-in only")
to prevent accidental enablement; refer to the invalidate_dependencies.enabled
keys under assets, documents and objects and adjust those entries accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/Unit/Element/DependencyInvalidatorTest.php`:
- Around line 41-42: The test is mocking TestObject->getType(), but
DependencyInvalidator::invalidate uses ElementType::tryFromElement which calls
Pimcore\Model\Element\Service::getElementType and relies on instanceof checks,
so the Prophecy mock causes getElementType() to return null and the test exits
early; fix by either (A) replacing the Prophecy mocks of TestObject with real
Pimcore element instances created via your test factories (the same approach
used in integration tests) so instanceof checks pass, or (B) if you want a pure
unit test, stub/mocks Pimcore\Model\Element\Service::getElementType to return
the appropriate ElementType for your fake element before calling
DependencyInvalidator::invalidate; apply the same change to all similar cases
referenced (lines around the other getType() mocks).

---

Nitpick comments:
In `@doc/2-configuration.md`:
- Around line 15-58: The YAML example currently shows
invalidate_dependencies.enabled set to true for assets, documents and objects
which contradicts the “disabled by default” comment; update the snippet so each
invalidate_dependencies.enabled is set to false (or clearly mark the shown true
values as an explicit opt-in example) and add a short inline comment next to
invalidate_dependencies.enabled (e.g., "# default: false — opt-in only") to
prevent accidental enablement; refer to the invalidate_dependencies.enabled keys
under assets, documents and objects and adjust those entries accordingly.

In `@src/DependencyInjection/Configuration.php`:
- Around line 40-55: The invalidate_dependencies subtree repeats identical
boolean child node definitions for 'assets', 'documents', and 'objects' under
the 'types' ArrayNode; refactor by extracting a small helper method (e.g.,
addTypeBooleanNodes or addBooleanChildrenToTypes) that takes the parent
ArrayNodeDefinition (the 'types' node) and an array of names and adds the
booleanNode(...)->defaultFalse()->end() entries, then replace each repeated
block (the three occurrences around invalidate_dependencies/types) with calls to
that helper to reduce duplication and future drift.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26c75fd and 00f70ab.

📒 Files selected for processing (33)
  • config/services.php
  • doc/2-configuration.md
  • doc/3-pimcore-elements.md
  • src/Cache/CacheTagChecker/Element/AssetCacheTagChecker.php
  • src/Cache/CacheTagChecker/Element/DocumentCacheTagChecker.php
  • src/Cache/CacheTagChecker/Element/ObjectCacheTagChecker.php
  • src/DependencyInjection/Configuration.php
  • src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php
  • src/Element/DependencyInvalidator.php
  • src/Element/ElementType.php
  • src/Element/ElementsConfig.php
  • src/Element/InvalidateElementListener.php
  • tests/Integration/Configuration/CollectConfigurationDataTest.php
  • tests/Integration/Helpers/TestAssetFactory.php
  • tests/Integration/Helpers/TestDocumentFactory.php
  • tests/Integration/Helpers/TestObjectFactory.php
  • tests/Integration/Invalidation/CancelInvalidationTest.php
  • tests/Integration/Invalidation/InvalidateAdditionalTagTest.php
  • tests/Integration/Invalidation/InvalidateAssetTest.php
  • tests/Integration/Invalidation/InvalidateDocumentTest.php
  • tests/Integration/Invalidation/InvalidateObjectTest.php
  • tests/Integration/Tagging/CollectTagsDataTest.php
  • tests/Integration/Tagging/TagAdditionalTagTest.php
  • tests/Integration/Tagging/TagAssetTest.php
  • tests/Integration/Tagging/TagDocumentTest.php
  • tests/Integration/Tagging/TagObjectTest.php
  • tests/Unit/Cache/CacheTagChecker/Element/AssetCacheTagCheckerTest.php
  • tests/Unit/Cache/CacheTagChecker/Element/DocumentCacheTagCheckerTest.php
  • tests/Unit/Cache/CacheTagChecker/Element/ObjectCacheTagCheckerTest.php
  • tests/Unit/Element/DependencyInvalidatorTest.php
  • tests/Unit/Element/InvalidateElementListenerTest.php
  • tests/app/config/pimcore/classes/definition_TestObject.php
  • tests/app/src/Controller/GetObjectController.php

Comment on lines +41 to +42
$element = $this->prophesize(TestObject::class);
$element->getType()->willReturn(ElementType::Object->value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the ElementType class
find . -name "ElementType.php" -type f

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 109


🏁 Script executed:

# Also check the test file to understand the context
find . -name "DependencyInvalidatorTest.php" -type f

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 130


🏁 Script executed:

# Search for Service::getElementType to understand how it works
rg "getElementType" --type php -B2 -A5

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 833


🏁 Script executed:

# Find the Service class that's being used
rg "class Service" --type php

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 62


🏁 Script executed:

# Look for the getElementType method definition
rg "function getElementType" --type php -B2 -A10

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 62


🏁 Script executed:

# Check what Service class is imported in ElementType.php
head -30 src/Element/ElementType.php

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 876


🏁 Script executed:

# Let's examine the test file to see the full context
cat -n tests/Unit/Element/DependencyInvalidatorTest.php | head -80

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 3364


🏁 Script executed:

# Find and examine the DependencyInvalidator class itself
find . -name "DependencyInvalidator.php" -type f

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 119


🏁 Script executed:

# Check if there are any stubs or mocks for Pimcore Service in the tests
rg "Service" tests/Unit/Element/DependencyInvalidatorTest.php -B2 -A2

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 62


🏁 Script executed:

# Examine the DependencyInvalidator implementation
cat -n src/Element/DependencyInvalidator.php

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 2101


🏁 Script executed:

# Check what TestObject is - is it a real Pimcore class or a test stub?
rg "class TestObject" --type php -B2 -A5

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 707


🏁 Script executed:

# Let's search for how Pimcore's Service::getElementType is typically implemented
# Check if there's any documentation or references to it in the codebase
rg "Service::getElementType" --type php -B3 -A3

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 769


🏁 Script executed:

# Check the composer.json to see what version of Pimcore is being used
cat composer.json | grep -i pimcore

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 606


🏁 Script executed:

# Let's also check if there are any integration tests that actually use real Pimcore objects
cat tests/Integration/Invalidation/CancelInvalidationTest.php | head -100

Repository: teamneusta/pimcore-http-cache-bundle

Length of output: 3281


Mock strategy incompatible with Service::getElementType().

Line 41-42 mocks $element->getType()->willReturn(...), but DependencyInvalidator::invalidate() calls ElementType::tryFromElement($source), which internally calls Pimcore\Model\Element\Service::getElementType($element). Pimcore's Service::getElementType() determines element type via instanceof checks on actual Pimcore objects, not by calling getType().

A Prophecy mock of TestObject won't pass these instanceof checks, causing Service::getElementType() to return null. The test then returns early at line 24 (if ($type === null || ...), passing for the wrong reason without testing the actual invalidation logic.

Use real Pimcore objects (via test factories, as in integration tests) instead of mocks, or mock Service::getElementType() directly if unit-testing this class in isolation.

Also applies to: lines 61-64, 86-89, 110-113, 134-137, 158-161, 185-188, 216-219, 241-245

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Unit/Element/DependencyInvalidatorTest.php` around lines 41 - 42, The
test is mocking TestObject->getType(), but DependencyInvalidator::invalidate
uses ElementType::tryFromElement which calls
Pimcore\Model\Element\Service::getElementType and relies on instanceof checks,
so the Prophecy mock causes getElementType() to return null and the test exits
early; fix by either (A) replacing the Prophecy mocks of TestObject with real
Pimcore element instances created via your test factories (the same approach
used in integration tests) so instanceof checks pass, or (B) if you want a pure
unit test, stub/mocks Pimcore\Model\Element\Service::getElementType to return
the appropriate ElementType for your fake element before calling
DependencyInvalidator::invalidate; apply the same change to all similar cases
referenced (lines around the other getType() mocks).

Jan Adams and others added 7 commits March 3, 2026 10:31
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 <noreply@anthropic.com>
… tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…e_dependent_elements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ilter 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 <noreply@anthropic.com>
@jan888adams jan888adams force-pushed the invalidate-dependent-elements branch from ed71e85 to 949ecb6 Compare March 4, 2026 09:39
Jan Adams and others added 8 commits March 4, 2026 11:11
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
…ve subtype tests

- 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 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
@jan888adams jan888adams force-pushed the invalidate-dependent-elements branch from cb12102 to 09b29b1 Compare March 4, 2026 10:45
…derTest

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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants