Skip to content

Commit 9ffba3d

Browse files
Jan Adamsclaude
andcommitted
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 <noreply@anthropic.com>
1 parent ad59529 commit 9ffba3d

5 files changed

Lines changed: 308 additions & 213 deletions

File tree

config/services.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\RemoveDisabledTagsResponseTagger;
1818
use Neusta\Pimcore\HttpCacheBundle\CacheActivator;
1919
use Neusta\Pimcore\HttpCacheBundle\DataCollector;
20+
use Neusta\Pimcore\HttpCacheBundle\Element\DependencyInvalidator;
2021
use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository;
2122
use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig;
2223
use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener;
@@ -95,11 +96,14 @@
9596
->arg('$responseTagger', service('neusta_pimcore_http_cache.response_tagger'))
9697
->arg('$dispatcher', service('event_dispatcher'));
9798

99+
$services->set('neusta_pimcore_http_cache.element.dependency_invalidator', DependencyInvalidator::class)
100+
->arg('$elementRepository', service('.neusta_pimcore_http_cache.element.repository'))
101+
->arg('$config', service('neusta_pimcore_http_cache.elements_config'));
102+
98103
$services->set('neusta_pimcore_http_cache.element.invalidate_listener', InvalidateElementListener::class)
99104
->arg('$cacheInvalidator', service('neusta_pimcore_http_cache.cache_invalidator'))
100105
->arg('$dispatcher', service('event_dispatcher'))
101-
->arg('$elementRepository', service('.neusta_pimcore_http_cache.element.repository'))
102-
->arg('$config', service('neusta_pimcore_http_cache.elements_config'));
106+
->arg('$dependencyInvalidator', service('neusta_pimcore_http_cache.element.dependency_invalidator'));
103107

104108
$services->set('neusta_pimcore_http_cache.data_collector', DataCollector::class)
105109
->arg('$traceableResponseTagger', service('.neusta_pimcore_http_cache.response_tagger.traceable'))
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Neusta\Pimcore\HttpCacheBundle\Element;
4+
5+
use Pimcore\Model\Element\ElementInterface;
6+
7+
final class DependencyInvalidator
8+
{
9+
public function __construct(
10+
private readonly ElementRepository $elementRepository,
11+
private readonly ElementsConfig $config,
12+
) {
13+
}
14+
15+
/**
16+
* Invalidates dependent elements one level deep.
17+
* Dependencies of dependent elements are intentionally not traversed to prevent cycles.
18+
*
19+
* @param callable(ElementInterface): bool $invalidate
20+
*/
21+
public function invalidate(ElementInterface $source, callable $invalidate): void
22+
{
23+
$type = ElementType::tryFromElement($source);
24+
if ($type === null || !$this->config->isDependencyTraversalEnabled($type)) {
25+
return;
26+
}
27+
28+
foreach ($source->getDependencies()->getRequiredBy() as $required) {
29+
if (!isset($required['id'], $required['type'])) {
30+
continue;
31+
}
32+
33+
$dependentType = ElementType::tryFrom($required['type']);
34+
if ($dependentType === null || !$this->config->isDependentTypeEnabled($type, $dependentType)) {
35+
continue;
36+
}
37+
38+
$element = match ($dependentType) {
39+
ElementType::Object => $this->elementRepository->findObject((int) $required['id']),
40+
ElementType::Document => $this->elementRepository->findDocument((int) $required['id']),
41+
ElementType::Asset => $this->elementRepository->findAsset((int) $required['id']),
42+
};
43+
44+
if ($element) {
45+
$invalidate($element);
46+
}
47+
}
48+
}
49+
}

src/Element/InvalidateElementListener.php

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use Neusta\Pimcore\HttpCacheBundle\Cache\CacheInvalidator;
66
use Pimcore\Event\Model\ElementEventInterface;
7-
use Pimcore\Model\Dependency;
87
use Pimcore\Model\Element\ElementInterface;
98
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
109

@@ -13,8 +12,7 @@ final class InvalidateElementListener
1312
public function __construct(
1413
private readonly CacheInvalidator $cacheInvalidator,
1514
private readonly EventDispatcherInterface $dispatcher,
16-
private readonly ElementRepository $elementRepository,
17-
private readonly ElementsConfig $config,
15+
private readonly DependencyInvalidator $dependencyInvalidator,
1816
) {
1917
}
2018

@@ -43,10 +41,7 @@ private function invalidateWithDependencies(ElementInterface $element): void
4341
return;
4442
}
4543

46-
$type = ElementType::tryFromElement($element);
47-
if ($type !== null && $this->config->isDependencyTraversalEnabled($type)) {
48-
$this->invalidateDependencies($element->getDependencies(), $type);
49-
}
44+
$this->dependencyInvalidator->invalidate($element, fn ($e) => $this->invalidateElement($e));
5045
}
5146

5247
private function invalidateElement(ElementInterface $element): bool
@@ -62,32 +57,4 @@ private function invalidateElement(ElementInterface $element): bool
6257

6358
return true;
6459
}
65-
66-
/**
67-
* Invalidates dependent elements one level deep.
68-
* Dependencies of dependent elements are intentionally not traversed to prevent cycles.
69-
*/
70-
private function invalidateDependencies(Dependency $dependency, ElementType $sourceType): void
71-
{
72-
foreach ($dependency->getRequiredBy() as $required) {
73-
if (!isset($required['id'], $required['type'])) {
74-
continue;
75-
}
76-
77-
$dependentType = ElementType::tryFrom($required['type']);
78-
if ($dependentType === null || !$this->config->isDependentTypeEnabled($sourceType, $dependentType)) {
79-
continue;
80-
}
81-
82-
$element = match ($dependentType) {
83-
ElementType::Object => $this->elementRepository->findObject((int) $required['id']),
84-
ElementType::Document => $this->elementRepository->findDocument((int) $required['id']),
85-
ElementType::Asset => $this->elementRepository->findAsset((int) $required['id']),
86-
};
87-
88-
if ($element) {
89-
$this->invalidateElement($element);
90-
}
91-
}
92-
}
9360
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Neusta\Pimcore\HttpCacheBundle\Tests\Unit\Element;
4+
5+
use Neusta\Pimcore\HttpCacheBundle\Element\DependencyInvalidator;
6+
use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository;
7+
use Neusta\Pimcore\HttpCacheBundle\Element\ElementsConfig;
8+
use Neusta\Pimcore\HttpCacheBundle\Element\ElementType;
9+
use PHPUnit\Framework\TestCase;
10+
use Pimcore\Model\Asset;
11+
use Pimcore\Model\DataObject;
12+
use Pimcore\Model\DataObject\TestObject;
13+
use Pimcore\Model\Dependency;
14+
use Pimcore\Model\Document;
15+
use Prophecy\Argument;
16+
use Prophecy\PhpUnit\ProphecyTrait;
17+
use Prophecy\Prophecy\ObjectProphecy;
18+
19+
final class DependencyInvalidatorTest extends TestCase
20+
{
21+
use ProphecyTrait;
22+
23+
/** @var ObjectProphecy<ElementRepository> */
24+
private ObjectProphecy $elementRepository;
25+
26+
protected function setUp(): void
27+
{
28+
$this->elementRepository = $this->prophesize(ElementRepository::class);
29+
}
30+
31+
/**
32+
* @test
33+
*/
34+
public function invalidate_does_nothing_when_traversal_is_disabled_for_source_type(): void
35+
{
36+
$invalidator = new DependencyInvalidator(
37+
$this->elementRepository->reveal(),
38+
ElementsConfig::fromArray([]),
39+
);
40+
41+
$element = $this->prophesize(TestObject::class);
42+
$element->getType()->willReturn(ElementType::Object->value);
43+
44+
$called = false;
45+
$invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; });
46+
47+
self::assertFalse($called);
48+
$this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled();
49+
}
50+
51+
/**
52+
* @test
53+
*/
54+
public function invalidate_skips_entries_without_id_or_type(): void
55+
{
56+
$invalidator = new DependencyInvalidator(
57+
$this->elementRepository->reveal(),
58+
ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]),
59+
);
60+
61+
$element = $this->prophesize(TestObject::class);
62+
$dependency = $this->prophesize(Dependency::class);
63+
$element->getType()->willReturn(ElementType::Object->value);
64+
$element->getDependencies()->willReturn($dependency->reveal());
65+
$dependency->getRequiredBy()->willReturn([
66+
['type' => 'object'], // missing id
67+
['id' => 23], // missing type
68+
[], // missing both
69+
]);
70+
71+
$called = false;
72+
$invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; });
73+
74+
self::assertFalse($called);
75+
$this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled();
76+
}
77+
78+
/**
79+
* @test
80+
*/
81+
public function invalidate_skips_entries_with_unknown_type(): void
82+
{
83+
$invalidator = new DependencyInvalidator(
84+
$this->elementRepository->reveal(),
85+
ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]),
86+
);
87+
88+
$element = $this->prophesize(TestObject::class);
89+
$dependency = $this->prophesize(Dependency::class);
90+
$element->getType()->willReturn(ElementType::Object->value);
91+
$element->getDependencies()->willReturn($dependency->reveal());
92+
$dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'unknown']]);
93+
94+
$called = false;
95+
$invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; });
96+
97+
self::assertFalse($called);
98+
$this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled();
99+
}
100+
101+
/**
102+
* @test
103+
*/
104+
public function invalidate_skips_entries_when_dependent_type_is_disabled(): void
105+
{
106+
$invalidator = new DependencyInvalidator(
107+
$this->elementRepository->reveal(),
108+
ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => false]]]]),
109+
);
110+
111+
$element = $this->prophesize(TestObject::class);
112+
$dependency = $this->prophesize(Dependency::class);
113+
$element->getType()->willReturn(ElementType::Object->value);
114+
$element->getDependencies()->willReturn($dependency->reveal());
115+
$dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]);
116+
117+
$called = false;
118+
$invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; });
119+
120+
self::assertFalse($called);
121+
$this->elementRepository->findObject(Argument::any())->shouldNotHaveBeenCalled();
122+
}
123+
124+
/**
125+
* @test
126+
*/
127+
public function invalidate_skips_dependent_element_when_not_found(): void
128+
{
129+
$invalidator = new DependencyInvalidator(
130+
$this->elementRepository->reveal(),
131+
ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]),
132+
);
133+
134+
$element = $this->prophesize(TestObject::class);
135+
$dependency = $this->prophesize(Dependency::class);
136+
$element->getType()->willReturn(ElementType::Object->value);
137+
$element->getDependencies()->willReturn($dependency->reveal());
138+
$dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]);
139+
$this->elementRepository->findObject(23)->willReturn(null);
140+
141+
$called = false;
142+
$invalidator->invalidate($element->reveal(), function () use (&$called) { $called = true; });
143+
144+
self::assertFalse($called);
145+
}
146+
147+
/**
148+
* @test
149+
*/
150+
public function invalidate_calls_callable_for_each_dependent_object(): void
151+
{
152+
$invalidator = new DependencyInvalidator(
153+
$this->elementRepository->reveal(),
154+
ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['objects' => true]]]]),
155+
);
156+
157+
$element = $this->prophesize(TestObject::class);
158+
$dependency = $this->prophesize(Dependency::class);
159+
$dependentElement = $this->prophesize(DataObject::class);
160+
$element->getType()->willReturn(ElementType::Object->value);
161+
$element->getDependencies()->willReturn($dependency->reveal());
162+
$dependentElement->getId()->willReturn(23);
163+
$dependency->getRequiredBy()->willReturn([['id' => 23, 'type' => 'object']]);
164+
$this->elementRepository->findObject(23)->willReturn($dependentElement->reveal());
165+
166+
$received = [];
167+
$invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; });
168+
169+
self::assertCount(1, $received);
170+
self::assertSame($dependentElement->reveal(), $received[0]);
171+
}
172+
173+
/**
174+
* @test
175+
*/
176+
public function invalidate_calls_callable_for_dependent_document(): void
177+
{
178+
$invalidator = new DependencyInvalidator(
179+
$this->elementRepository->reveal(),
180+
ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['documents' => true]]]]),
181+
);
182+
183+
$element = $this->prophesize(TestObject::class);
184+
$dependency = $this->prophesize(Dependency::class);
185+
$dependentDocument = $this->prophesize(Document::class);
186+
$element->getType()->willReturn(ElementType::Object->value);
187+
$element->getDependencies()->willReturn($dependency->reveal());
188+
$dependentDocument->getId()->willReturn(5);
189+
$dependency->getRequiredBy()->willReturn([['id' => 5, 'type' => 'document']]);
190+
$this->elementRepository->findDocument(5)->willReturn($dependentDocument->reveal());
191+
192+
$received = [];
193+
$invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; });
194+
195+
self::assertCount(1, $received);
196+
self::assertSame($dependentDocument->reveal(), $received[0]);
197+
}
198+
199+
/**
200+
* @test
201+
*/
202+
public function invalidate_calls_callable_for_dependent_asset(): void
203+
{
204+
$invalidator = new DependencyInvalidator(
205+
$this->elementRepository->reveal(),
206+
ElementsConfig::fromArray(['objects' => ['invalidate_dependencies' => ['enabled' => true, 'types' => ['assets' => true]]]]),
207+
);
208+
209+
$element = $this->prophesize(TestObject::class);
210+
$dependency = $this->prophesize(Dependency::class);
211+
$dependentAsset = $this->prophesize(Asset::class);
212+
$element->getType()->willReturn(ElementType::Object->value);
213+
$element->getDependencies()->willReturn($dependency->reveal());
214+
$dependentAsset->getId()->willReturn(7);
215+
$dependency->getRequiredBy()->willReturn([['id' => 7, 'type' => 'asset']]);
216+
$this->elementRepository->findAsset(7)->willReturn($dependentAsset->reveal());
217+
218+
$received = [];
219+
$invalidator->invalidate($element->reveal(), function ($e) use (&$received) { $received[] = $e; });
220+
221+
self::assertCount(1, $received);
222+
self::assertSame($dependentAsset->reveal(), $received[0]);
223+
}
224+
}

0 commit comments

Comments
 (0)