From 1f4f3cfde56220f88c899efdc19cb0efe87cb608 Mon Sep 17 00:00:00 2001 From: Sztig Date: Fri, 15 May 2026 16:44:38 +0200 Subject: [PATCH 1/4] added event to handle cache invalidation on files that have been updated but the content version has not changed --- src/bundle/Resources/config/event.yml | 8 ++- .../BinaryFileHttpCachePurgeSubscriber.php | 55 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriber.php diff --git a/src/bundle/Resources/config/event.yml b/src/bundle/Resources/config/event.yml index 2e6a23e..3ff1888 100644 --- a/src/bundle/Resources/config/event.yml +++ b/src/bundle/Resources/config/event.yml @@ -12,9 +12,15 @@ services: $contentHandler: '@Ibexa\Core\Persistence\Cache\ContentHandler' $isTranslationAware: '%ibexa.http_cache.translation_aware.enabled%' + Ibexa\HttpCache\EventSubscriber\CachePurge\BinaryFileHttpCachePurgeSubscriber: + arguments: + $cacheManager: '@fos_http_cache.cache_manager' + Ibexa\HttpCache\EventSubscriber\CachePurge\: resource: '../../../lib/EventSubscriber/CachePurge/*' - exclude: '../../../lib/EventSubscriber/CachePurge/ContentEventsSubscriber.php' + exclude: + - '../../../lib/EventSubscriber/CachePurge/ContentEventsSubscriber.php' + - '../../../lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriber.php' arguments: $purgeClient: '@ibexa.http_cache.purge_client' $locationHandler: '@Ibexa\Core\Persistence\Cache\LocationHandler' diff --git a/src/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriber.php b/src/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriber.php new file mode 100644 index 0000000..b6e317f --- /dev/null +++ b/src/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriber.php @@ -0,0 +1,55 @@ +cacheManager = $cacheManager; + } + + public static function getSubscribedEvents(): array + { + return [ + PublishVersionEvent::class => 'onPublishVersion', + ]; + } + + public function onPublishVersion(PublishVersionEvent $event): void + { + $content = $event->getContent(); + $purged = []; + + foreach ($content->getFields() as $field) { + $value = $field->value; + + if (!$value instanceof ImageValue && !$value instanceof BinaryBaseValue) { + continue; + } + + $uri = $value->uri; + + if ($uri === null || $uri === '' || isset($purged[$uri])) { + continue; + } + + $this->cacheManager->invalidatePath($uri); + $purged[$uri] = true; + } + } +} From 4960e6e503ba98a45ff9c49030f3ed1e54b25229 Mon Sep 17 00:00:00 2001 From: Sztig Date: Fri, 15 May 2026 16:45:38 +0200 Subject: [PATCH 2/4] added test coverage --- ...BinaryFileHttpCachePurgeSubscriberTest.php | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 tests/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriberTest.php diff --git a/tests/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriberTest.php b/tests/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriberTest.php new file mode 100644 index 0000000..cca0eb3 --- /dev/null +++ b/tests/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriberTest.php @@ -0,0 +1,189 @@ +cacheManager = $this->getMockBuilder(CacheManager::class) + ->setConstructorArgs([ + $this->createMock(ProxyClient::class), + $this->createMock(UrlGeneratorInterface::class), + ]) + ->getMock(); + + $this->subscriber = new BinaryFileHttpCachePurgeSubscriber($this->cacheManager); + } + + public function testGetSubscribedEvents(): void + { + self::assertArrayHasKey(PublishVersionEvent::class, BinaryFileHttpCachePurgeSubscriber::getSubscribedEvents()); + } + + /** + * @param \Ibexa\Contracts\Core\Repository\Values\Content\Field[] $fields + */ + private function buildEvent(array $fields): PublishVersionEvent + { + $content = $this->createMock(Content::class); + $content->method('getFields')->willReturn($fields); + + return new PublishVersionEvent( + $content, + $this->createMock(VersionInfo::class), + [], + ); + } + + public function testNoFieldsDoesNotCallInvalidatePath(): void + { + $this->cacheManager->expects(self::never())->method('invalidatePath'); + + $this->subscriber->onPublishVersion($this->buildEvent([])); + } + + public function testNonBinaryFieldIsSkipped(): void + { + $this->cacheManager->expects(self::never())->method('invalidatePath'); + + $field = new Field(['value' => new \stdClass()]); + $this->subscriber->onPublishVersion($this->buildEvent([$field])); + } + + public function testImageValueWithUriIsInvalidated(): void + { + $imageValue = new ImageValue(); + $imageValue->uri = '/var/site/storage/images/foo.jpg'; + + $this->cacheManager + ->expects(self::once()) + ->method('invalidatePath') + ->with('/var/site/storage/images/foo.jpg'); + + $this->subscriber->onPublishVersion($this->buildEvent([ + new Field(['value' => $imageValue]), + ])); + } + + public function testBinaryFileValueWithUriIsInvalidated(): void + { + $binaryValue = new BinaryFileValue(); + $binaryValue->uri = '/var/site/storage/original/application/foo.pdf'; + + $this->cacheManager + ->expects(self::once()) + ->method('invalidatePath') + ->with('/var/site/storage/original/application/foo.pdf'); + + $this->subscriber->onPublishVersion($this->buildEvent([ + new Field(['value' => $binaryValue]), + ])); + } + + public function testImageValueWithNullUriIsSkipped(): void + { + $imageValue = new ImageValue(); + $imageValue->uri = null; + + $this->cacheManager->expects(self::never())->method('invalidatePath'); + + $this->subscriber->onPublishVersion($this->buildEvent([ + new Field(['value' => $imageValue]), + ])); + } + + public function testImageValueWithEmptyUriIsSkipped(): void + { + $imageValue = new ImageValue(); + $imageValue->uri = ''; + + $this->cacheManager->expects(self::never())->method('invalidatePath'); + + $this->subscriber->onPublishVersion($this->buildEvent([ + new Field(['value' => $imageValue]), + ])); + } + + public function testDuplicateUriIsInvalidatedOnlyOnce(): void + { + $uri = '/var/site/storage/images/same.jpg'; + + $imageValue1 = new ImageValue(); + $imageValue1->uri = $uri; + + $imageValue2 = new ImageValue(); + $imageValue2->uri = $uri; + + $this->cacheManager + ->expects(self::once()) + ->method('invalidatePath') + ->with($uri); + + $this->subscriber->onPublishVersion($this->buildEvent([ + new Field(['value' => $imageValue1]), + new Field(['value' => $imageValue2]), + ])); + } + + public function testMultipleDistinctUrisAreEachInvalidated(): void + { + $imageValue = new ImageValue(); + $imageValue->uri = '/var/site/storage/images/a.jpg'; + + $binaryValue = new BinaryFileValue(); + $binaryValue->uri = '/var/site/storage/original/application/b.pdf'; + + $this->cacheManager + ->expects(self::exactly(2)) + ->method('invalidatePath') + ->withConsecutive( + ['/var/site/storage/images/a.jpg'], + ['/var/site/storage/original/application/b.pdf'], + ); + + $this->subscriber->onPublishVersion($this->buildEvent([ + new Field(['value' => $imageValue]), + new Field(['value' => $binaryValue]), + ])); + } + + public function testMixedFieldsOnlyInvalidatesBinaryAndImageUris(): void + { + $imageValue = new ImageValue(); + $imageValue->uri = '/var/site/storage/images/photo.jpg'; + + $this->cacheManager + ->expects(self::once()) + ->method('invalidatePath') + ->with('/var/site/storage/images/photo.jpg'); + + $this->subscriber->onPublishVersion($this->buildEvent([ + new Field(['value' => 'plain text value']), + new Field(['value' => $imageValue]), + new Field(['value' => 42]), + ])); + } +} From 24208c279d85cbce00b990a5a7976d97c93852e5 Mon Sep 17 00:00:00 2001 From: Sztig Date: Mon, 18 May 2026 12:13:21 +0200 Subject: [PATCH 3/4] implemented suggested mocking changes --- ...BinaryFileHttpCachePurgeSubscriberTest.php | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/tests/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriberTest.php b/tests/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriberTest.php index cca0eb3..da5052e 100644 --- a/tests/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriberTest.php +++ b/tests/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriberTest.php @@ -8,7 +8,7 @@ namespace Ibexa\Tests\HttpCache\EventSubscriber\CachePurge; -use FOS\HttpCache\ProxyClient\ProxyClient; +use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable; use FOS\HttpCacheBundle\CacheManager; use Ibexa\Contracts\Core\Repository\Events\Content\PublishVersionEvent; use Ibexa\Contracts\Core\Repository\Values\Content\Content; @@ -22,25 +22,27 @@ final class BinaryFileHttpCachePurgeSubscriberTest extends TestCase { - private CacheManager $cacheManager; + private PurgeCapable $proxyClient; private BinaryFileHttpCachePurgeSubscriber $subscriber; protected function setUp(): void { - $this->cacheManager = $this->getMockBuilder(CacheManager::class) - ->setConstructorArgs([ - $this->createMock(ProxyClient::class), - $this->createMock(UrlGeneratorInterface::class), - ]) - ->getMock(); - - $this->subscriber = new BinaryFileHttpCachePurgeSubscriber($this->cacheManager); + $this->proxyClient = $this->createMock(PurgeCapable::class); + $this->subscriber = new BinaryFileHttpCachePurgeSubscriber( + new CacheManager( + $this->proxyClient, + $this->createMock(UrlGeneratorInterface::class) + ), + ); } public function testGetSubscribedEvents(): void { - self::assertArrayHasKey(PublishVersionEvent::class, BinaryFileHttpCachePurgeSubscriber::getSubscribedEvents()); + self::assertArrayHasKey( + PublishVersionEvent::class, + BinaryFileHttpCachePurgeSubscriber::getSubscribedEvents() + ); } /** @@ -58,19 +60,20 @@ private function buildEvent(array $fields): PublishVersionEvent ); } - public function testNoFieldsDoesNotCallInvalidatePath(): void + public function testNoFieldsDoesNotCallPurge(): void { - $this->cacheManager->expects(self::never())->method('invalidatePath'); + $this->proxyClient->expects(self::never())->method('purge'); $this->subscriber->onPublishVersion($this->buildEvent([])); } public function testNonBinaryFieldIsSkipped(): void { - $this->cacheManager->expects(self::never())->method('invalidatePath'); + $this->proxyClient->expects(self::never())->method('purge'); - $field = new Field(['value' => new \stdClass()]); - $this->subscriber->onPublishVersion($this->buildEvent([$field])); + $this->subscriber->onPublishVersion($this->buildEvent([ + new Field(['value' => new \stdClass()]), + ])); } public function testImageValueWithUriIsInvalidated(): void @@ -78,10 +81,10 @@ public function testImageValueWithUriIsInvalidated(): void $imageValue = new ImageValue(); $imageValue->uri = '/var/site/storage/images/foo.jpg'; - $this->cacheManager + $this->proxyClient ->expects(self::once()) - ->method('invalidatePath') - ->with('/var/site/storage/images/foo.jpg'); + ->method('purge') + ->with('/var/site/storage/images/foo.jpg', []); $this->subscriber->onPublishVersion($this->buildEvent([ new Field(['value' => $imageValue]), @@ -93,10 +96,10 @@ public function testBinaryFileValueWithUriIsInvalidated(): void $binaryValue = new BinaryFileValue(); $binaryValue->uri = '/var/site/storage/original/application/foo.pdf'; - $this->cacheManager + $this->proxyClient ->expects(self::once()) - ->method('invalidatePath') - ->with('/var/site/storage/original/application/foo.pdf'); + ->method('purge') + ->with('/var/site/storage/original/application/foo.pdf', []); $this->subscriber->onPublishVersion($this->buildEvent([ new Field(['value' => $binaryValue]), @@ -108,7 +111,7 @@ public function testImageValueWithNullUriIsSkipped(): void $imageValue = new ImageValue(); $imageValue->uri = null; - $this->cacheManager->expects(self::never())->method('invalidatePath'); + $this->proxyClient->expects(self::never())->method('purge'); $this->subscriber->onPublishVersion($this->buildEvent([ new Field(['value' => $imageValue]), @@ -120,7 +123,7 @@ public function testImageValueWithEmptyUriIsSkipped(): void $imageValue = new ImageValue(); $imageValue->uri = ''; - $this->cacheManager->expects(self::never())->method('invalidatePath'); + $this->proxyClient->expects(self::never())->method('purge'); $this->subscriber->onPublishVersion($this->buildEvent([ new Field(['value' => $imageValue]), @@ -137,10 +140,10 @@ public function testDuplicateUriIsInvalidatedOnlyOnce(): void $imageValue2 = new ImageValue(); $imageValue2->uri = $uri; - $this->cacheManager + $this->proxyClient ->expects(self::once()) - ->method('invalidatePath') - ->with($uri); + ->method('purge') + ->with($uri, []); $this->subscriber->onPublishVersion($this->buildEvent([ new Field(['value' => $imageValue1]), @@ -156,12 +159,12 @@ public function testMultipleDistinctUrisAreEachInvalidated(): void $binaryValue = new BinaryFileValue(); $binaryValue->uri = '/var/site/storage/original/application/b.pdf'; - $this->cacheManager + $this->proxyClient ->expects(self::exactly(2)) - ->method('invalidatePath') + ->method('purge') ->withConsecutive( - ['/var/site/storage/images/a.jpg'], - ['/var/site/storage/original/application/b.pdf'], + ['/var/site/storage/images/a.jpg', []], + ['/var/site/storage/original/application/b.pdf', []], ); $this->subscriber->onPublishVersion($this->buildEvent([ @@ -175,10 +178,10 @@ public function testMixedFieldsOnlyInvalidatesBinaryAndImageUris(): void $imageValue = new ImageValue(); $imageValue->uri = '/var/site/storage/images/photo.jpg'; - $this->cacheManager + $this->proxyClient ->expects(self::once()) - ->method('invalidatePath') - ->with('/var/site/storage/images/photo.jpg'); + ->method('purge') + ->with('/var/site/storage/images/photo.jpg', []); $this->subscriber->onPublishVersion($this->buildEvent([ new Field(['value' => 'plain text value']), From 50d9234adf9380e69a36e0fdac713eab572e8df5 Mon Sep 17 00:00:00 2001 From: Sztig Date: Mon, 18 May 2026 12:17:11 +0200 Subject: [PATCH 4/4] browser test fix --- features/setup/symfonyCache.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/setup/symfonyCache.feature b/features/setup/symfonyCache.feature index 1827b25..808023c 100644 --- a/features/setup/symfonyCache.feature +++ b/features/setup/symfonyCache.feature @@ -26,7 +26,7 @@ index 9982c21..03ac40a 100644 require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - return function (array $context) { + return static function (array $context) { - return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); + $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); + Request::enableHttpMethodParameterOverride();