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(); 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; + } + } +} diff --git a/tests/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriberTest.php b/tests/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriberTest.php new file mode 100644 index 0000000..da5052e --- /dev/null +++ b/tests/lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriberTest.php @@ -0,0 +1,192 @@ +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() + ); + } + + /** + * @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 testNoFieldsDoesNotCallPurge(): void + { + $this->proxyClient->expects(self::never())->method('purge'); + + $this->subscriber->onPublishVersion($this->buildEvent([])); + } + + public function testNonBinaryFieldIsSkipped(): void + { + $this->proxyClient->expects(self::never())->method('purge'); + + $this->subscriber->onPublishVersion($this->buildEvent([ + new Field(['value' => new \stdClass()]), + ])); + } + + public function testImageValueWithUriIsInvalidated(): void + { + $imageValue = new ImageValue(); + $imageValue->uri = '/var/site/storage/images/foo.jpg'; + + $this->proxyClient + ->expects(self::once()) + ->method('purge') + ->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->proxyClient + ->expects(self::once()) + ->method('purge') + ->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->proxyClient->expects(self::never())->method('purge'); + + $this->subscriber->onPublishVersion($this->buildEvent([ + new Field(['value' => $imageValue]), + ])); + } + + public function testImageValueWithEmptyUriIsSkipped(): void + { + $imageValue = new ImageValue(); + $imageValue->uri = ''; + + $this->proxyClient->expects(self::never())->method('purge'); + + $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->proxyClient + ->expects(self::once()) + ->method('purge') + ->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->proxyClient + ->expects(self::exactly(2)) + ->method('purge') + ->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->proxyClient + ->expects(self::once()) + ->method('purge') + ->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]), + ])); + } +}