From 31c7b18c17b47c0525e860615142362a14f54a6a Mon Sep 17 00:00:00 2001 From: konradoboza Date: Wed, 1 Apr 2026 09:51:28 +0200 Subject: [PATCH 01/11] IBX-11553: Made response taggers more verbose instead of silently failing --- phpstan-baseline.neon | 64 +++++++++---------- .../Delegator/ContentValueViewTaggerSpec.php | 17 ++++- .../Delegator/DispatcherTaggerSpec.php | 46 +++++++++---- .../Delegator/LocationValueViewTaggerSpec.php | 9 ++- .../Value/ContentInfoTaggerSpec.php | 13 ++-- .../Value/LocationTaggerSpec.php | 12 ++-- .../Compiler/ResponseTaggersPass.php | 3 +- .../ResponseTagger/ResponseTagger.php | 5 +- .../Delegator/ContentValueViewTagger.php | 19 +++--- .../Delegator/DispatcherTagger.php | 16 +++-- .../Delegator/LocationValueViewTagger.php | 19 +++--- .../Value/AbstractValueTagger.php | 8 +-- .../Value/ContentInfoTagger.php | 16 +++-- .../ResponseTagger/Value/LocationTagger.php | 18 +++--- 14 files changed, 163 insertions(+), 102 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e2c0a59..a6ba142 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -609,7 +609,13 @@ parameters: - message: '#^Call to an undefined method Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Content\:\:willReturn\(\)\.$#' identifier: method.notFound - count: 1 + count: 2 + path: spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php + + - + message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Delegator\\ContentValueViewTaggerSpec\:\:supports\(\)\.$#' + identifier: method.notFound + count: 2 path: spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php - @@ -627,7 +633,13 @@ parameters: - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Delegator\\DispatcherTaggerSpec\:\:tag\(\)\.$#' identifier: method.notFound - count: 1 + count: 2 + path: spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php + + - + message: '#^Cannot call method willReturn\(\) on bool\.$#' + identifier: method.nonObject + count: 4 path: spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php - @@ -642,6 +654,12 @@ parameters: count: 1 path: spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php + - + message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Delegator\\LocationValueViewTaggerSpec\:\:supports\(\)\.$#' + identifier: method.notFound + count: 1 + path: spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php + - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Delegator\\LocationValueViewTaggerSpec\:\:tag\(\)\.$#' identifier: method.notFound @@ -661,21 +679,21 @@ parameters: path: spec/ResponseTagger/Value/ContentInfoTaggerSpec.php - - message: '#^Call to an undefined method FOS\\HttpCache\\ResponseTagger\:\:shouldNotHaveBeenCalled\(\)\.$#' + message: '#^Call to an undefined method FOS\\HttpCache\\ResponseTagger\:\:willReturn\(\)\.$#' identifier: method.notFound count: 1 path: spec/ResponseTagger/Value/ContentInfoTaggerSpec.php - - message: '#^Call to an undefined method FOS\\HttpCache\\ResponseTagger\:\:willReturn\(\)\.$#' + message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Value\\ContentInfoTaggerSpec\:\:supports\(\)\.$#' identifier: method.notFound - count: 1 + count: 2 path: spec/ResponseTagger/Value/ContentInfoTaggerSpec.php - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Value\\ContentInfoTaggerSpec\:\:tag\(\)\.$#' identifier: method.notFound - count: 3 + count: 2 path: spec/ResponseTagger/Value/ContentInfoTaggerSpec.php - @@ -684,12 +702,6 @@ parameters: count: 1 path: spec/ResponseTagger/Value/ContentInfoTaggerSpec.php - - - message: '#^Method FOS\\HttpCache\\ResponseTagger\:\:addTags\(\) invoked with 0 parameters, 1 required\.$#' - identifier: arguments.count - count: 1 - path: spec/ResponseTagger/Value/ContentInfoTaggerSpec.php - - message: '#^Parameter \#1 \$tags of method FOS\\HttpCache\\ResponseTagger\:\:addTags\(\) expects array\, Prophecy\\Argument\\Token\\AnyValueToken given\.$#' identifier: argument.type @@ -703,21 +715,21 @@ parameters: path: spec/ResponseTagger/Value/LocationTaggerSpec.php - - message: '#^Call to an undefined method FOS\\HttpCache\\ResponseTagger\:\:shouldNotHaveBeenCalled\(\)\.$#' + message: '#^Call to an undefined method FOS\\HttpCache\\ResponseTagger\:\:willReturn\(\)\.$#' identifier: method.notFound count: 1 path: spec/ResponseTagger/Value/LocationTaggerSpec.php - - message: '#^Call to an undefined method FOS\\HttpCache\\ResponseTagger\:\:willReturn\(\)\.$#' + message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Value\\LocationTaggerSpec\:\:supports\(\)\.$#' identifier: method.notFound - count: 1 + count: 2 path: spec/ResponseTagger/Value/LocationTaggerSpec.php - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Value\\LocationTaggerSpec\:\:tag\(\)\.$#' identifier: method.notFound - count: 4 + count: 3 path: spec/ResponseTagger/Value/LocationTaggerSpec.php - @@ -729,7 +741,7 @@ parameters: - message: '#^Parameter \#1 \$tags of method FOS\\HttpCache\\ResponseTagger\:\:addTags\(\) expects array\, Prophecy\\Argument\\Token\\AnyValueToken given\.$#' identifier: argument.type - count: 2 + count: 1 path: spec/ResponseTagger/Value/LocationTaggerSpec.php - @@ -1122,24 +1134,6 @@ parameters: count: 1 path: src/lib/ResponseTagger/Delegator/ContentValueViewTagger.php - - - message: '#^Method Ibexa\\HttpCache\\ResponseTagger\\Delegator\\DispatcherTagger\:\:__construct\(\) has parameter \$taggers with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/lib/ResponseTagger/Delegator/DispatcherTagger.php - - - - message: '#^PHPDoc tag @var for property Ibexa\\HttpCache\\ResponseTagger\\Delegator\\DispatcherTagger\:\:\$taggers with type Ibexa\\Contracts\\HttpCache\\ResponseTagger\\ResponseTagger is incompatible with native type array\.$#' - identifier: property.phpDocType - count: 1 - path: src/lib/ResponseTagger/Delegator/DispatcherTagger.php - - - - message: '#^Property Ibexa\\HttpCache\\ResponseTagger\\Delegator\\DispatcherTagger\:\:\$taggers type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/lib/ResponseTagger/Delegator/DispatcherTagger.php - - message: '#^Method Ibexa\\HttpCache\\ResponseTagger\\Delegator\\LocationValueViewTagger\:\:tag\(\) has no return type specified\.$#' identifier: missingType.return diff --git a/spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php b/spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php index ce6bc8a..6b42478 100644 --- a/spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php +++ b/spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php @@ -4,6 +4,7 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace spec\Ibexa\HttpCache\ResponseTagger\Delegator; @@ -11,11 +12,12 @@ use Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger; use Ibexa\Core\MVC\Symfony\View\ContentValueView; use Ibexa\Core\Repository\Values\Content\Content; +use Ibexa\Core\Repository\Values\Content\Location; use Ibexa\Core\Repository\Values\Content\VersionInfo; use Ibexa\HttpCache\ResponseTagger\Delegator\ContentValueViewTagger; use PhpSpec\ObjectBehavior; -class ContentValueViewTaggerSpec extends ObjectBehavior +final class ContentValueViewTaggerSpec extends ObjectBehavior { public function let(ResponseTagger $contentInfoTagger): void { @@ -27,6 +29,19 @@ public function it_is_initializable(): void $this->shouldHaveType(ContentValueViewTagger::class); } + public function it_supports_content_value_view_with_content(ContentValueView $view): void + { + $content = new Content(['versionInfo' => new VersionInfo(['contentInfo' => new ContentInfo()])]); + $view->getContent()->willReturn($content); + + $this->supports($view)->shouldReturn(true); + } + + public function it_does_not_support_non_content_value_view(): void + { + $this->supports(new Location())->shouldReturn(false); + } + public function it_delegates_tagging_of_the_content_info( ResponseTagger $contentInfoTagger, ContentValueView $view diff --git a/spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php b/spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php index 452af93..1d021a0 100644 --- a/spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php +++ b/spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php @@ -4,19 +4,22 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace spec\Ibexa\HttpCache\ResponseTagger\Delegator; -use Ibexa\Contracts\Core\Repository\Values\ValueObject; -use Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger; +use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo; +use Ibexa\Core\Repository\Values\Content\Location; use Ibexa\HttpCache\ResponseTagger\Delegator\DispatcherTagger; +use Ibexa\HttpCache\ResponseTagger\Value\ContentInfoTagger; +use Ibexa\HttpCache\ResponseTagger\Value\LocationTagger; use PhpSpec\ObjectBehavior; -class DispatcherTaggerSpec extends ObjectBehavior +final class DispatcherTaggerSpec extends ObjectBehavior { - public function let(ResponseTagger $taggerOne, ResponseTagger $taggerTwo): void + public function let(ContentInfoTagger $contentInfoTagger, LocationTagger $locationTagger): void { - $this->beConstructedWith([$taggerOne, $taggerTwo]); + $this->beConstructedWith([$contentInfoTagger, $locationTagger]); } public function it_is_initializable(): void @@ -24,14 +27,33 @@ public function it_is_initializable(): void $this->shouldHaveType(DispatcherTagger::class); } - public function it_calls_tag_on_every_tagger( - ResponseTagger $taggerOne, - ResponseTagger $taggerTwo, - ValueObject $value + public function it_calls_tag_only_on_taggers_that_support_the_value( + ContentInfoTagger $contentInfoTagger, + LocationTagger $locationTagger ): void { - $this->tag($value); + $contentInfo = new ContentInfo(['id' => 1, 'contentTypeId' => 2]); - $taggerOne->tag($value)->shouldHaveBeenCalled(); - $taggerTwo->tag($value)->shouldHaveBeenCalled(); + $contentInfoTagger->supports($contentInfo)->willReturn(true); + $locationTagger->supports($contentInfo)->willReturn(false); + + $this->tag($contentInfo); + + $contentInfoTagger->tag($contentInfo)->shouldHaveBeenCalled(); + $locationTagger->tag($contentInfo)->shouldNotHaveBeenCalled(); + } + + public function it_does_not_call_tag_when_no_tagger_supports_the_value( + ContentInfoTagger $contentInfoTagger, + LocationTagger $locationTagger + ): void { + $location = new Location(['id' => 1]); + + $contentInfoTagger->supports($location)->willReturn(false); + $locationTagger->supports($location)->willReturn(false); + + $this->tag($location); + + $contentInfoTagger->tag($location)->shouldNotHaveBeenCalled(); + $locationTagger->tag($location)->shouldNotHaveBeenCalled(); } } diff --git a/spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php b/spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php index c750bb5..a1c2aa8 100644 --- a/spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php +++ b/spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php @@ -4,16 +4,18 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace spec\Ibexa\HttpCache\ResponseTagger\Delegator; +use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo; use Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger; use Ibexa\Core\MVC\Symfony\View\LocationValueView; use Ibexa\Core\Repository\Values\Content\Location; use Ibexa\HttpCache\ResponseTagger\Delegator\LocationValueViewTagger; use PhpSpec\ObjectBehavior; -class LocationValueViewTaggerSpec extends ObjectBehavior +final class LocationValueViewTaggerSpec extends ObjectBehavior { public function let(ResponseTagger $locationTagger): void { @@ -25,6 +27,11 @@ public function it_is_initializable(): void $this->shouldHaveType(LocationValueViewTagger::class); } + public function it_does_not_support_non_location_value_view(): void + { + $this->supports(new ContentInfo())->shouldReturn(false); + } + public function it_delegates_tagging_of_the_location( ResponseTagger $locationTagger, LocationValueView $view diff --git a/spec/ResponseTagger/Value/ContentInfoTaggerSpec.php b/spec/ResponseTagger/Value/ContentInfoTaggerSpec.php index 021e0be..98988b7 100644 --- a/spec/ResponseTagger/Value/ContentInfoTaggerSpec.php +++ b/spec/ResponseTagger/Value/ContentInfoTaggerSpec.php @@ -4,16 +4,18 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace spec\Ibexa\HttpCache\ResponseTagger\Value; use FOS\HttpCache\ResponseTagger; use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo; +use Ibexa\Core\Repository\Values\Content\Location; use Ibexa\HttpCache\ResponseTagger\Value\ContentInfoTagger; use PhpSpec\ObjectBehavior; use Prophecy\Argument; -class ContentInfoTaggerSpec extends ObjectBehavior +final class ContentInfoTaggerSpec extends ObjectBehavior { public function let(ResponseTagger $tagHandler): void { @@ -27,11 +29,14 @@ public function it_is_initializable(): void $this->shouldHaveType(ContentInfoTagger::class); } - public function it_ignores_non_content_info(ResponseTagger $tagHandler): void + public function it_supports_content_info(): void { - $this->tag(null); + $this->supports(new ContentInfo())->shouldReturn(true); + } - $tagHandler->addTags()->shouldNotHaveBeenCalled(); + public function it_does_not_support_non_content_info(): void + { + $this->supports(new Location())->shouldReturn(false); } public function it_tags_with_content_and_content_type_id(ResponseTagger $tagHandler): void diff --git a/spec/ResponseTagger/Value/LocationTaggerSpec.php b/spec/ResponseTagger/Value/LocationTaggerSpec.php index 7acfb0c..837154e 100644 --- a/spec/ResponseTagger/Value/LocationTaggerSpec.php +++ b/spec/ResponseTagger/Value/LocationTaggerSpec.php @@ -4,6 +4,7 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace spec\Ibexa\HttpCache\ResponseTagger\Value; @@ -14,7 +15,7 @@ use PhpSpec\ObjectBehavior; use Prophecy\Argument; -class LocationTaggerSpec extends ObjectBehavior +final class LocationTaggerSpec extends ObjectBehavior { public function let(ResponseTagger $tagHandler): void { @@ -28,11 +29,14 @@ public function it_is_initializable(): void $this->shouldHaveType(LocationTagger::class); } - public function it_ignores_non_location(ResponseTagger $tagHandler): void + public function it_supports_location(): void { - $this->tag(null); + $this->supports(new Location())->shouldReturn(true); + } - $tagHandler->addTags(Argument::any())->shouldNotHaveBeenCalled(); + public function it_does_not_support_non_location(): void + { + $this->supports(new ContentInfo())->shouldReturn(false); } public function it_tags_with_location_id_if_not_main_location(ResponseTagger $tagHandler): void diff --git a/src/bundle/DependencyInjection/Compiler/ResponseTaggersPass.php b/src/bundle/DependencyInjection/Compiler/ResponseTaggersPass.php index c85e8a3..86336ca 100644 --- a/src/bundle/DependencyInjection/Compiler/ResponseTaggersPass.php +++ b/src/bundle/DependencyInjection/Compiler/ResponseTaggersPass.php @@ -4,6 +4,7 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace Ibexa\Bundle\HttpCache\DependencyInjection\Compiler; @@ -15,7 +16,7 @@ /** * Injects services tagged as "ibexa.cache.http.response.tagger" into the dispatcher. */ -class ResponseTaggersPass implements CompilerPassInterface +final readonly class ResponseTaggersPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { diff --git a/src/contracts/ResponseTagger/ResponseTagger.php b/src/contracts/ResponseTagger/ResponseTagger.php index 9e251cc..407d0ba 100644 --- a/src/contracts/ResponseTagger/ResponseTagger.php +++ b/src/contracts/ResponseTagger/ResponseTagger.php @@ -4,6 +4,7 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace Ibexa\Contracts\HttpCache\ResponseTagger; @@ -14,8 +15,6 @@ interface ResponseTagger { /** * Extracts tags from a value. - * - * @param mixed $value */ - public function tag($value); + public function tag(mixed $value); } diff --git a/src/lib/ResponseTagger/Delegator/ContentValueViewTagger.php b/src/lib/ResponseTagger/Delegator/ContentValueViewTagger.php index a749b3b..3bdda75 100644 --- a/src/lib/ResponseTagger/Delegator/ContentValueViewTagger.php +++ b/src/lib/ResponseTagger/Delegator/ContentValueViewTagger.php @@ -4,27 +4,30 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace Ibexa\HttpCache\ResponseTagger\Delegator; use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger; use Ibexa\Core\MVC\Symfony\View\ContentValueView; +use Ibexa\HttpCache\ResponseTagger\Value\AbstractValueTagger; -class ContentValueViewTagger implements ResponseTagger +class ContentValueViewTagger extends AbstractValueTagger { - private ResponseTagger $contentInfoTagger; + public function __construct(private readonly ResponseTagger $contentInfoTagger) + { + } - public function __construct(ResponseTagger $contentInfoTagger) + public function supports(mixed $value): bool { - $this->contentInfoTagger = $contentInfoTagger; + return $value instanceof ContentValueView && $value->getContent() instanceof Content; } - public function tag($view) + public function tag(mixed $value) { - if (!$view instanceof ContentValueView || !($content = $view->getContent()) instanceof Content) { - return $this; - } + /** @var \Ibexa\Core\MVC\Symfony\View\ContentValueView $value */ + $content = $value->getContent(); $contentInfo = $content->getVersionInfo()->getContentInfo(); $this->contentInfoTagger->tag($contentInfo); diff --git a/src/lib/ResponseTagger/Delegator/DispatcherTagger.php b/src/lib/ResponseTagger/Delegator/DispatcherTagger.php index 292d685..3ed0696 100644 --- a/src/lib/ResponseTagger/Delegator/DispatcherTagger.php +++ b/src/lib/ResponseTagger/Delegator/DispatcherTagger.php @@ -8,25 +8,27 @@ namespace Ibexa\HttpCache\ResponseTagger\Delegator; use Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger; +use Ibexa\HttpCache\ResponseTagger\Value\AbstractValueTagger; /** * Dispatches a value to all registered ResponseTaggers. */ -class DispatcherTagger implements ResponseTagger +readonly class DispatcherTagger implements ResponseTagger { /** - * @var \Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger + * @param \Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger[] $taggers */ - private array $taggers; - - public function __construct(array $taggers = []) + public function __construct(private array $taggers = []) { - $this->taggers = $taggers; } - public function tag($value): void + public function tag(mixed $value): void { foreach ($this->taggers as $tagger) { + if (!$tagger instanceof AbstractValueTagger || !$tagger->supports($value)) { + continue; + } + $tagger->tag($value); } } diff --git a/src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php b/src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php index 7481bd1..571f655 100644 --- a/src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php +++ b/src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php @@ -4,27 +4,30 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace Ibexa\HttpCache\ResponseTagger\Delegator; use Ibexa\Contracts\Core\Repository\Values\Content\Location; use Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger; use Ibexa\Core\MVC\Symfony\View\LocationValueView; +use Ibexa\HttpCache\ResponseTagger\Value\AbstractValueTagger; -class LocationValueViewTagger implements ResponseTagger +class LocationValueViewTagger extends AbstractValueTagger { - private ResponseTagger $locationTagger; + public function __construct(private readonly ResponseTagger $locationTagger) + { + } - public function __construct(ResponseTagger $locationTagger) + public function supports(mixed $value): bool { - $this->locationTagger = $locationTagger; + return $value instanceof LocationValueView && !$value->getLocation() instanceof Location; } - public function tag($view) + public function tag(mixed $value) { - if (!$view instanceof LocationValueView || !($location = $view->getLocation()) instanceof Location) { - return $this; - } + /** @var \Ibexa\Core\MVC\Symfony\View\LocationValueView $value */ + $location = $value->getLocation(); $this->locationTagger->tag($location); } diff --git a/src/lib/ResponseTagger/Value/AbstractValueTagger.php b/src/lib/ResponseTagger/Value/AbstractValueTagger.php index e84eb80..89b722b 100644 --- a/src/lib/ResponseTagger/Value/AbstractValueTagger.php +++ b/src/lib/ResponseTagger/Value/AbstractValueTagger.php @@ -4,6 +4,7 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace Ibexa\HttpCache\ResponseTagger\Value; @@ -12,10 +13,9 @@ abstract class AbstractValueTagger implements ResponseTagger { - protected FosResponseTagger $responseTagger; - - public function __construct(FosResponseTagger $responseTagger) + public function __construct(protected FosResponseTagger $responseTagger) { - $this->responseTagger = $responseTagger; } + + abstract public function supports(mixed $value): bool; } diff --git a/src/lib/ResponseTagger/Value/ContentInfoTagger.php b/src/lib/ResponseTagger/Value/ContentInfoTagger.php index ba1846d..eae361d 100644 --- a/src/lib/ResponseTagger/Value/ContentInfoTagger.php +++ b/src/lib/ResponseTagger/Value/ContentInfoTagger.php @@ -4,6 +4,7 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace Ibexa\HttpCache\ResponseTagger\Value; @@ -12,19 +13,22 @@ class ContentInfoTagger extends AbstractValueTagger { - public function tag($value) + public function supports(mixed $value): bool { - if (!$value instanceof ContentInfo) { - return $this; - } + return $value instanceof ContentInfo; + } + public function tag(mixed $value) + { $this->responseTagger->addTags([ - ContentTagInterface::CONTENT_PREFIX . $value->id, + ContentTagInterface::CONTENT_PREFIX . $value->getId(), ContentTagInterface::CONTENT_TYPE_PREFIX . $value->contentTypeId, ]); if ($value->mainLocationId) { - $this->responseTagger->addTags([ContentTagInterface::LOCATION_PREFIX . $value->mainLocationId]); + $this->responseTagger->addTags([ + ContentTagInterface::LOCATION_PREFIX . $value->getMainLocationId(), + ]); } } } diff --git a/src/lib/ResponseTagger/Value/LocationTagger.php b/src/lib/ResponseTagger/Value/LocationTagger.php index 5ab85e5..6e7b10a 100644 --- a/src/lib/ResponseTagger/Value/LocationTagger.php +++ b/src/lib/ResponseTagger/Value/LocationTagger.php @@ -4,6 +4,7 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace Ibexa\HttpCache\ResponseTagger\Value; @@ -12,23 +13,24 @@ class LocationTagger extends AbstractValueTagger { - public function tag($value) + public function supports(mixed $value): bool { - if (!$value instanceof Location) { - return $this; - } + return $value instanceof Location; + } - if ($value->id !== $value->contentInfo->mainLocationId) { - $this->responseTagger->addTags([ContentTagInterface::LOCATION_PREFIX . $value->id]); + public function tag(mixed $value) + { + if ($value->id !== $value->getContentInfo()->getMainLocationId()) { + $this->responseTagger->addTags([ContentTagInterface::LOCATION_PREFIX . $value->getId()]); } $this->responseTagger->addTags([ContentTagInterface::PARENT_LOCATION_PREFIX . $value->parentLocationId]); $this->responseTagger->addTags( array_map( - static function (string $pathItem): string { + static function ($pathItem): string { return ContentTagInterface::PATH_PREFIX . $pathItem; }, - $value->path + $value->getPath() ) ); } From dc1eced65e745ddc27395d936dafb5f6d24a7a04 Mon Sep 17 00:00:00 2001 From: konradoboza Date: Wed, 1 Apr 2026 16:45:19 +0200 Subject: [PATCH 02/11] replaced spec tests with regular, PHPUnit ones --- phpstan-baseline.neon | 138 ------------------ .../Delegator/ContentValueViewTaggerSpec.php | 57 -------- .../Delegator/DispatcherTaggerSpec.php | 59 -------- .../Delegator/LocationValueViewTaggerSpec.php | 45 ------ .../Value/ContentInfoTaggerSpec.php | 67 --------- .../Value/LocationTaggerSpec.php | 81 ---------- .../Delegator/ContentValueViewTaggerTest.php | 61 ++++++++ .../Delegator/DispatcherTaggerTest.php | 53 +++++++ .../Delegator/LocationValueViewTaggerTest.php | 49 +++++++ .../Value/ContentInfoTaggerTest.php | 80 ++++++++++ .../Value/LocationTaggerTest.php | 133 +++++++++++++++++ 11 files changed, 376 insertions(+), 447 deletions(-) delete mode 100644 spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php delete mode 100644 spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php delete mode 100644 spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php delete mode 100644 spec/ResponseTagger/Value/ContentInfoTaggerSpec.php delete mode 100644 spec/ResponseTagger/Value/LocationTaggerSpec.php create mode 100644 tests/lib/ResponseTagger/Delegator/ContentValueViewTaggerTest.php create mode 100644 tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php create mode 100644 tests/lib/ResponseTagger/Delegator/LocationValueViewTaggerTest.php create mode 100644 tests/lib/ResponseTagger/Value/ContentInfoTaggerTest.php create mode 100644 tests/lib/ResponseTagger/Value/LocationTaggerTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a6ba142..3f9b554 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -606,144 +606,6 @@ parameters: count: 1 path: spec/ResponseConfigurator/ConfigurableResponseCacheConfiguratorSpec.php - - - message: '#^Call to an undefined method Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Content\:\:willReturn\(\)\.$#' - identifier: method.notFound - count: 2 - path: spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php - - - - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Delegator\\ContentValueViewTaggerSpec\:\:supports\(\)\.$#' - identifier: method.notFound - count: 2 - path: spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php - - - - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Delegator\\ContentValueViewTaggerSpec\:\:tag\(\)\.$#' - identifier: method.notFound - count: 1 - path: spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php - - - - message: '#^Class spec\\Ibexa\\HttpCache\\ResponseTagger\\Delegator\\ContentValueViewTaggerSpec extends generic class PhpSpec\\ObjectBehavior but does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php - - - - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Delegator\\DispatcherTaggerSpec\:\:tag\(\)\.$#' - identifier: method.notFound - count: 2 - path: spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php - - - - message: '#^Cannot call method willReturn\(\) on bool\.$#' - identifier: method.nonObject - count: 4 - path: spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php - - - - message: '#^Class spec\\Ibexa\\HttpCache\\ResponseTagger\\Delegator\\DispatcherTaggerSpec extends generic class PhpSpec\\ObjectBehavior but does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php - - - - message: '#^Call to an undefined method Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Location\:\:willReturn\(\)\.$#' - identifier: method.notFound - count: 1 - path: spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php - - - - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Delegator\\LocationValueViewTaggerSpec\:\:supports\(\)\.$#' - identifier: method.notFound - count: 1 - path: spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php - - - - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Delegator\\LocationValueViewTaggerSpec\:\:tag\(\)\.$#' - identifier: method.notFound - count: 1 - path: spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php - - - - message: '#^Class spec\\Ibexa\\HttpCache\\ResponseTagger\\Delegator\\LocationValueViewTaggerSpec extends generic class PhpSpec\\ObjectBehavior but does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php - - - - message: '#^Call to an undefined method FOS\\HttpCache\\ResponseTagger\:\:shouldHaveBeenCalled\(\)\.$#' - identifier: method.notFound - count: 2 - path: spec/ResponseTagger/Value/ContentInfoTaggerSpec.php - - - - message: '#^Call to an undefined method FOS\\HttpCache\\ResponseTagger\:\:willReturn\(\)\.$#' - identifier: method.notFound - count: 1 - path: spec/ResponseTagger/Value/ContentInfoTaggerSpec.php - - - - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Value\\ContentInfoTaggerSpec\:\:supports\(\)\.$#' - identifier: method.notFound - count: 2 - path: spec/ResponseTagger/Value/ContentInfoTaggerSpec.php - - - - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Value\\ContentInfoTaggerSpec\:\:tag\(\)\.$#' - identifier: method.notFound - count: 2 - path: spec/ResponseTagger/Value/ContentInfoTaggerSpec.php - - - - message: '#^Class spec\\Ibexa\\HttpCache\\ResponseTagger\\Value\\ContentInfoTaggerSpec extends generic class PhpSpec\\ObjectBehavior but does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: spec/ResponseTagger/Value/ContentInfoTaggerSpec.php - - - - message: '#^Parameter \#1 \$tags of method FOS\\HttpCache\\ResponseTagger\:\:addTags\(\) expects array\, Prophecy\\Argument\\Token\\AnyValueToken given\.$#' - identifier: argument.type - count: 1 - path: spec/ResponseTagger/Value/ContentInfoTaggerSpec.php - - - - message: '#^Call to an undefined method FOS\\HttpCache\\ResponseTagger\:\:shouldHaveBeenCalled\(\)\.$#' - identifier: method.notFound - count: 3 - path: spec/ResponseTagger/Value/LocationTaggerSpec.php - - - - message: '#^Call to an undefined method FOS\\HttpCache\\ResponseTagger\:\:willReturn\(\)\.$#' - identifier: method.notFound - count: 1 - path: spec/ResponseTagger/Value/LocationTaggerSpec.php - - - - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Value\\LocationTaggerSpec\:\:supports\(\)\.$#' - identifier: method.notFound - count: 2 - path: spec/ResponseTagger/Value/LocationTaggerSpec.php - - - - message: '#^Call to an undefined method spec\\Ibexa\\HttpCache\\ResponseTagger\\Value\\LocationTaggerSpec\:\:tag\(\)\.$#' - identifier: method.notFound - count: 3 - path: spec/ResponseTagger/Value/LocationTaggerSpec.php - - - - message: '#^Class spec\\Ibexa\\HttpCache\\ResponseTagger\\Value\\LocationTaggerSpec extends generic class PhpSpec\\ObjectBehavior but does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: spec/ResponseTagger/Value/LocationTaggerSpec.php - - - - message: '#^Parameter \#1 \$tags of method FOS\\HttpCache\\ResponseTagger\:\:addTags\(\) expects array\, Prophecy\\Argument\\Token\\AnyValueToken given\.$#' - identifier: argument.type - count: 1 - path: spec/ResponseTagger/Value/LocationTaggerSpec.php - - message: '#^Call to an undefined method Symfony\\Component\\HttpKernel\\HttpKernelInterface\:\:isDebug\(\)\.$#' identifier: method.notFound diff --git a/spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php b/spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php deleted file mode 100644 index 6b42478..0000000 --- a/spec/ResponseTagger/Delegator/ContentValueViewTaggerSpec.php +++ /dev/null @@ -1,57 +0,0 @@ -beConstructedWith($contentInfoTagger); - } - - public function it_is_initializable(): void - { - $this->shouldHaveType(ContentValueViewTagger::class); - } - - public function it_supports_content_value_view_with_content(ContentValueView $view): void - { - $content = new Content(['versionInfo' => new VersionInfo(['contentInfo' => new ContentInfo()])]); - $view->getContent()->willReturn($content); - - $this->supports($view)->shouldReturn(true); - } - - public function it_does_not_support_non_content_value_view(): void - { - $this->supports(new Location())->shouldReturn(false); - } - - public function it_delegates_tagging_of_the_content_info( - ResponseTagger $contentInfoTagger, - ContentValueView $view - ): void { - $contentInfo = new ContentInfo(); - $content = new Content(['versionInfo' => new VersionInfo(['contentInfo' => $contentInfo])]); - $view->getContent()->willReturn($content); - - $this->tag($view); - - $contentInfoTagger->tag($contentInfo)->shouldHaveBeenCalled(); - } -} diff --git a/spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php b/spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php deleted file mode 100644 index 1d021a0..0000000 --- a/spec/ResponseTagger/Delegator/DispatcherTaggerSpec.php +++ /dev/null @@ -1,59 +0,0 @@ -beConstructedWith([$contentInfoTagger, $locationTagger]); - } - - public function it_is_initializable(): void - { - $this->shouldHaveType(DispatcherTagger::class); - } - - public function it_calls_tag_only_on_taggers_that_support_the_value( - ContentInfoTagger $contentInfoTagger, - LocationTagger $locationTagger - ): void { - $contentInfo = new ContentInfo(['id' => 1, 'contentTypeId' => 2]); - - $contentInfoTagger->supports($contentInfo)->willReturn(true); - $locationTagger->supports($contentInfo)->willReturn(false); - - $this->tag($contentInfo); - - $contentInfoTagger->tag($contentInfo)->shouldHaveBeenCalled(); - $locationTagger->tag($contentInfo)->shouldNotHaveBeenCalled(); - } - - public function it_does_not_call_tag_when_no_tagger_supports_the_value( - ContentInfoTagger $contentInfoTagger, - LocationTagger $locationTagger - ): void { - $location = new Location(['id' => 1]); - - $contentInfoTagger->supports($location)->willReturn(false); - $locationTagger->supports($location)->willReturn(false); - - $this->tag($location); - - $contentInfoTagger->tag($location)->shouldNotHaveBeenCalled(); - $locationTagger->tag($location)->shouldNotHaveBeenCalled(); - } -} diff --git a/spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php b/spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php deleted file mode 100644 index a1c2aa8..0000000 --- a/spec/ResponseTagger/Delegator/LocationValueViewTaggerSpec.php +++ /dev/null @@ -1,45 +0,0 @@ -beConstructedWith($locationTagger); - } - - public function it_is_initializable(): void - { - $this->shouldHaveType(LocationValueViewTagger::class); - } - - public function it_does_not_support_non_location_value_view(): void - { - $this->supports(new ContentInfo())->shouldReturn(false); - } - - public function it_delegates_tagging_of_the_location( - ResponseTagger $locationTagger, - LocationValueView $view - ): void { - $location = new Location(); - $view->getLocation()->willReturn($location); - $this->tag($view); - - $locationTagger->tag($location)->shouldHaveBeenCalled(); - } -} diff --git a/spec/ResponseTagger/Value/ContentInfoTaggerSpec.php b/spec/ResponseTagger/Value/ContentInfoTaggerSpec.php deleted file mode 100644 index 98988b7..0000000 --- a/spec/ResponseTagger/Value/ContentInfoTaggerSpec.php +++ /dev/null @@ -1,67 +0,0 @@ -beConstructedWith($tagHandler); - - $tagHandler->addTags(Argument::any())->willReturn($tagHandler); - } - - public function it_is_initializable(): void - { - $this->shouldHaveType(ContentInfoTagger::class); - } - - public function it_supports_content_info(): void - { - $this->supports(new ContentInfo())->shouldReturn(true); - } - - public function it_does_not_support_non_content_info(): void - { - $this->supports(new Location())->shouldReturn(false); - } - - public function it_tags_with_content_and_content_type_id(ResponseTagger $tagHandler): void - { - $value = new ContentInfo([ - 'id' => 123, - 'mainLocationId' => 456, - 'contentTypeId' => 987, - ]); - - $this->tag($value); - - $tagHandler->addTags(['c123', 'ct987'])->shouldHaveBeenCalled(); - } - - public function it_tags_with_location_id_if_one_is_set(ResponseTagger $tagHandler): void - { - $value = new ContentInfo([ - 'id' => 123, - 'mainLocationId' => 456, - 'contentTypeId' => 987, - ]); - - $this->tag($value); - - $tagHandler->addTags(['l456'])->shouldHaveBeenCalled(); - } -} diff --git a/spec/ResponseTagger/Value/LocationTaggerSpec.php b/spec/ResponseTagger/Value/LocationTaggerSpec.php deleted file mode 100644 index 837154e..0000000 --- a/spec/ResponseTagger/Value/LocationTaggerSpec.php +++ /dev/null @@ -1,81 +0,0 @@ -beConstructedWith($tagHandler); - - $tagHandler->addTags(Argument::any())->willReturn($tagHandler); - } - - public function it_is_initializable(): void - { - $this->shouldHaveType(LocationTagger::class); - } - - public function it_supports_location(): void - { - $this->supports(new Location())->shouldReturn(true); - } - - public function it_does_not_support_non_location(): void - { - $this->supports(new ContentInfo())->shouldReturn(false); - } - - public function it_tags_with_location_id_if_not_main_location(ResponseTagger $tagHandler): void - { - $value = new Location([ - 'id' => 123, - 'contentInfo' => new ContentInfo(['mainLocationId' => 321]), - 'parentLocationId' => 456, - ]); - - $this->tag($value); - - $tagHandler->addTags(['l123'])->shouldHaveBeenCalled(); - } - - public function it_tags_with_parent_location_id(ResponseTagger $tagHandler): void - { - $value = new Location([ - 'id' => 8, - 'parentLocationId' => 123, - 'contentInfo' => new ContentInfo(), - ]); - - $this->tag($value); - - $tagHandler->addTags(['pl123'])->shouldHaveBeenCalled(); - } - - public function it_tags_with_path_items(ResponseTagger $tagHandler): void - { - $value = new Location([ - 'id' => 4, - 'parentLocationId' => 123, - 'pathString' => '/1/2/123', - 'contentInfo' => new ContentInfo(), - ]); - - $this->tag($value); - - $tagHandler->addTags(['p1', 'p2', 'p123'])->shouldHaveBeenCalled(); - } -} diff --git a/tests/lib/ResponseTagger/Delegator/ContentValueViewTaggerTest.php b/tests/lib/ResponseTagger/Delegator/ContentValueViewTaggerTest.php new file mode 100644 index 0000000..a204099 --- /dev/null +++ b/tests/lib/ResponseTagger/Delegator/ContentValueViewTaggerTest.php @@ -0,0 +1,61 @@ +contentInfoTagger = $this->createMock(ResponseTagger::class); + $this->tagger = new ContentValueViewTagger($this->contentInfoTagger); + } + + public function testSupportsContentValueViewWithContent(): void + { + $view = $this->createMock(ContentValueView::class); + $content = new Content(['versionInfo' => new VersionInfo(['contentInfo' => new ContentInfo()])]); + $view->method('getContent')->willReturn($content); + + self::assertTrue($this->tagger->supports($view)); + } + + public function testDoesNotSupportNonContentValueView(): void + { + self::assertFalse($this->tagger->supports(new Location())); + } + + public function testDelegatesTaggingOfContentInfo(): void + { + $contentInfo = new ContentInfo(['id' => 42]); + $content = new Content(['versionInfo' => new VersionInfo(['contentInfo' => $contentInfo])]); + + $view = $this->createMock(ContentValueView::class); + $view->method('getContent')->willReturn($content); + + $this->contentInfoTagger + ->expects(self::once()) + ->method('tag') + ->with($contentInfo); + + $this->tagger->tag($view); + } +} diff --git a/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php b/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php new file mode 100644 index 0000000..394740d --- /dev/null +++ b/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php @@ -0,0 +1,53 @@ + 1, 'contentTypeId' => 2]); + + $contentInfoTagger = $this->createMock(ContentInfoTagger::class); + $locationTagger = $this->createMock(LocationTagger::class); + + $contentInfoTagger->method('supports')->with($contentInfo)->willReturn(true); + $locationTagger->method('supports')->with($contentInfo)->willReturn(false); + + $contentInfoTagger->expects(self::once())->method('tag')->with($contentInfo); + $locationTagger->expects(self::never())->method('tag'); + + $dispatcher = new DispatcherTagger([$contentInfoTagger, $locationTagger]); + $dispatcher->tag($contentInfo); + } + + public function testDoesNotCallTagWhenNoTaggerSupportsTheValue(): void + { + $location = new Location(['id' => 1]); + + $contentInfoTagger = $this->createMock(ContentInfoTagger::class); + $locationTagger = $this->createMock(LocationTagger::class); + + $contentInfoTagger->method('supports')->with($location)->willReturn(false); + $locationTagger->method('supports')->with($location)->willReturn(false); + + $contentInfoTagger->expects(self::never())->method('tag'); + $locationTagger->expects(self::never())->method('tag'); + + $dispatcher = new DispatcherTagger([$contentInfoTagger, $locationTagger]); + $dispatcher->tag($location); + } +} diff --git a/tests/lib/ResponseTagger/Delegator/LocationValueViewTaggerTest.php b/tests/lib/ResponseTagger/Delegator/LocationValueViewTaggerTest.php new file mode 100644 index 0000000..afc1aae --- /dev/null +++ b/tests/lib/ResponseTagger/Delegator/LocationValueViewTaggerTest.php @@ -0,0 +1,49 @@ +locationTagger = $this->createMock(ResponseTagger::class); + $this->tagger = new LocationValueViewTagger($this->locationTagger); + } + + public function testDoesNotSupportNonLocationValueView(): void + { + self::assertFalse($this->tagger->supports(new ContentInfo())); + } + + public function testDelegatesTaggingOfLocation(): void + { + $location = new Location(['id' => 55]); + + $view = $this->createMock(LocationValueView::class); + $view->method('getLocation')->willReturn($location); + + $this->locationTagger + ->expects(self::once()) + ->method('tag') + ->with($location); + + $this->tagger->tag($view); + } +} diff --git a/tests/lib/ResponseTagger/Value/ContentInfoTaggerTest.php b/tests/lib/ResponseTagger/Value/ContentInfoTaggerTest.php new file mode 100644 index 0000000..b28a690 --- /dev/null +++ b/tests/lib/ResponseTagger/Value/ContentInfoTaggerTest.php @@ -0,0 +1,80 @@ +responseTagger = $this->createMock(ResponseTagger::class); + $this->tagger = new ContentInfoTagger($this->responseTagger); + } + + public function testSupportsContentInfo(): void + { + self::assertTrue($this->tagger->supports(new ContentInfo())); + } + + public function testDoesNotSupportNonContentInfo(): void + { + self::assertFalse($this->tagger->supports(new Location())); + } + + public function testTagsWithContentAndContentTypeId(): void + { + $value = new ContentInfo(['id' => 123, 'contentTypeId' => 987, 'mainLocationId' => null]); + + $this->responseTagger + ->expects(self::once()) + ->method('addTags') + ->with([ + ContentTagInterface::CONTENT_PREFIX . '123', + ContentTagInterface::CONTENT_TYPE_PREFIX . '987', + ]); + + $this->tagger->tag($value); + } + + public function testTagsWithLocationIdWhenMainLocationIsSet(): void + { + $value = new ContentInfo(['id' => 1, 'contentTypeId' => 2, 'mainLocationId' => 456]); + + $this->responseTagger + ->expects(self::exactly(2)) + ->method('addTags') + ->withConsecutive( + [[ContentTagInterface::CONTENT_PREFIX . '1', ContentTagInterface::CONTENT_TYPE_PREFIX . '2']], + [[ContentTagInterface::LOCATION_PREFIX . '456']], + ); + + $this->tagger->tag($value); + } + + public function testDoesNotTagLocationWhenMainLocationIsNull(): void + { + $value = new ContentInfo(['id' => 1, 'contentTypeId' => 2, 'mainLocationId' => null]); + + $this->responseTagger + ->expects(self::once()) + ->method('addTags'); + + $this->tagger->tag($value); + } +} diff --git a/tests/lib/ResponseTagger/Value/LocationTaggerTest.php b/tests/lib/ResponseTagger/Value/LocationTaggerTest.php new file mode 100644 index 0000000..699fab7 --- /dev/null +++ b/tests/lib/ResponseTagger/Value/LocationTaggerTest.php @@ -0,0 +1,133 @@ +responseTagger = $this->createMock(ResponseTagger::class); + $this->tagger = new LocationTagger($this->responseTagger); + } + + public function testSupportsLocation(): void + { + self::assertTrue($this->tagger->supports(new Location())); + } + + public function testDoesNotSupportNonLocation(): void + { + self::assertFalse($this->tagger->supports(new ContentInfo())); + } + + public function testTagsWithLocationIdWhenNotMainLocation(): void + { + $location = new Location([ + 'id' => 123, + 'parentLocationId' => 2, + 'pathString' => '/1/2/123/', + 'contentInfo' => new ContentInfo(['mainLocationId' => 321]), + ]); + + $this->responseTagger + ->expects(self::exactly(3)) + ->method('addTags') + ->withConsecutive( + [[ContentTagInterface::LOCATION_PREFIX . '123']], + [[ContentTagInterface::PARENT_LOCATION_PREFIX . '2']], + [[ + ContentTagInterface::PATH_PREFIX . '1', + ContentTagInterface::PATH_PREFIX . '2', + ContentTagInterface::PATH_PREFIX . '123', + ]], + ); + + $this->tagger->tag($location); + } + + public function testDoesNotTagLocationIdWhenItIsMainLocation(): void + { + $location = new Location([ + 'id' => 55, + 'parentLocationId' => 2, + 'pathString' => '/1/2/55/', + 'contentInfo' => new ContentInfo(['mainLocationId' => 55]), + ]); + + $this->responseTagger + ->expects(self::exactly(2)) + ->method('addTags') + ->withConsecutive( + [[ContentTagInterface::PARENT_LOCATION_PREFIX . '2']], + [[ + ContentTagInterface::PATH_PREFIX . '1', + ContentTagInterface::PATH_PREFIX . '2', + ContentTagInterface::PATH_PREFIX . '55', + ]], + ); + + $this->tagger->tag($location); + } + + public function testTagsWithParentLocationId(): void + { + $location = new Location([ + 'id' => 4, + 'parentLocationId' => 123, + 'pathString' => '/1/123/4/', + 'contentInfo' => new ContentInfo(['mainLocationId' => null]), + ]); + + $this->responseTagger + ->expects(self::atLeastOnce()) + ->method('addTags') + ->withConsecutive( + [self::anything()], + [[ContentTagInterface::PARENT_LOCATION_PREFIX . '123']], + ); + + $this->tagger->tag($location); + } + + public function testTagsWithPathItems(): void + { + $location = new Location([ + 'id' => 4, + 'parentLocationId' => 2, + 'pathString' => '/1/2/123/', + 'contentInfo' => new ContentInfo(['mainLocationId' => null]), + ]); + + $this->responseTagger + ->expects(self::atLeastOnce()) + ->method('addTags') + ->withConsecutive( + [self::anything()], + [self::anything()], + [[ + ContentTagInterface::PATH_PREFIX . '1', + ContentTagInterface::PATH_PREFIX . '2', + ContentTagInterface::PATH_PREFIX . '123', + ]], + ); + + $this->tagger->tag($location); + } +} From 18cbf8181f2b8fb6c1c622d5590061b0d89fcd47 Mon Sep 17 00:00:00 2001 From: konradoboza Date: Thu, 2 Apr 2026 09:28:16 +0200 Subject: [PATCH 03/11] improved logic to keep BC, provided dedicated test cases --- .../Delegator/DispatcherTagger.php | 8 +++--- .../Delegator/DispatcherTaggerTest.php | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/lib/ResponseTagger/Delegator/DispatcherTagger.php b/src/lib/ResponseTagger/Delegator/DispatcherTagger.php index 3ed0696..84c5ef2 100644 --- a/src/lib/ResponseTagger/Delegator/DispatcherTagger.php +++ b/src/lib/ResponseTagger/Delegator/DispatcherTagger.php @@ -25,11 +25,11 @@ public function __construct(private array $taggers = []) public function tag(mixed $value): void { foreach ($this->taggers as $tagger) { - if (!$tagger instanceof AbstractValueTagger || !$tagger->supports($value)) { - continue; + // AbstractValueTagger subclasses declare supports() and should only tag matching values. + // Custom ResponseTagger implementations lack supports() for BC reasons and are always called. + if (!$tagger instanceof AbstractValueTagger || $tagger->supports($value)) { + $tagger->tag($value); } - - $tagger->tag($value); } } } diff --git a/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php b/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php index 394740d..aa4a824 100644 --- a/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php +++ b/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php @@ -9,11 +9,13 @@ namespace Ibexa\Tests\HttpCache\ResponseTagger\Delegator; use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo; +use Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger; use Ibexa\Core\Repository\Values\Content\Location; use Ibexa\HttpCache\ResponseTagger\Delegator\DispatcherTagger; use Ibexa\HttpCache\ResponseTagger\Value\ContentInfoTagger; use Ibexa\HttpCache\ResponseTagger\Value\LocationTagger; use PHPUnit\Framework\TestCase; +use stdClass; final class DispatcherTaggerTest extends TestCase { @@ -50,4 +52,30 @@ public function testDoesNotCallTagWhenNoTaggerSupportsTheValue(): void $dispatcher = new DispatcherTagger([$contentInfoTagger, $locationTagger]); $dispatcher->tag($location); } + + public function testCustomResponseTaggerImplementationLackingSupportsMethodShouldTag(): void + { + $foo = new stdClass(); + + $contentInfoTagger = $this->createMock(ContentInfoTagger::class); + $contentInfoTagger->method('supports')->with($foo)->willReturn(false); + $contentInfoTagger->expects(self::never())->method('tag'); + + $wasCalled = false; + $customTagger = new class($wasCalled) implements ResponseTagger { + public function __construct(private bool &$wasCalled) + { + } + + public function tag(mixed $value): void + { + $this->wasCalled = true; + } + }; + + $dispatcher = new DispatcherTagger([$contentInfoTagger, $customTagger]); + $dispatcher->tag($foo); + + self::assertTrue($wasCalled, 'Custom ResponseTagger::tag() was not called by the dispatcher.'); + } } From 8b742545fce9fca277bdc65c2ce3938a74bddc60 Mon Sep 17 00:00:00 2001 From: konradoboza Date: Thu, 2 Apr 2026 10:05:23 +0200 Subject: [PATCH 04/11] added possibility to fetch existing response taggers --- .../Delegator/DispatcherTagger.php | 13 +++++++++++ .../Delegator/DispatcherTaggerTest.php | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/lib/ResponseTagger/Delegator/DispatcherTagger.php b/src/lib/ResponseTagger/Delegator/DispatcherTagger.php index 84c5ef2..e2ebceb 100644 --- a/src/lib/ResponseTagger/Delegator/DispatcherTagger.php +++ b/src/lib/ResponseTagger/Delegator/DispatcherTagger.php @@ -32,4 +32,17 @@ public function tag(mixed $value): void } } } + + public function __toString(): string + { + $taggers = implode( + ', ', + array_map( + static fn (ResponseTagger $tagger): string => get_debug_type($tagger), + $this->taggers + ) + ); + + return sprintf('Available response taggers are: %s', $taggers); + } } diff --git a/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php b/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php index aa4a824..368c420 100644 --- a/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php +++ b/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php @@ -8,6 +8,7 @@ namespace Ibexa\Tests\HttpCache\ResponseTagger\Delegator; +use FOS\HttpCache\ResponseTagger as FosResponseTagger; use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo; use Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger; use Ibexa\Core\Repository\Values\Content\Location; @@ -78,4 +79,26 @@ public function tag(mixed $value): void self::assertTrue($wasCalled, 'Custom ResponseTagger::tag() was not called by the dispatcher.'); } + + public function testToStringWithNoTaggers(): void + { + $dispatcher = new DispatcherTagger(); + + self::assertSame('Available response taggers are: ', (string)$dispatcher); + } + + public function testToStringListsRegisteredTaggerTypes(): void + { + $fosResponseTagger = $this->createMock(FosResponseTagger::class); + + $dispatcher = new DispatcherTagger([ + new ContentInfoTagger($fosResponseTagger), + new LocationTagger($fosResponseTagger), + ]); + + self::assertSame( + 'Available response taggers are: Ibexa\HttpCache\ResponseTagger\Value\ContentInfoTagger, Ibexa\HttpCache\ResponseTagger\Value\LocationTagger', + (string)$dispatcher + ); + } } From 75b1184c0ed944a9e6710e0a0c7156f1d845640e Mon Sep 17 00:00:00 2001 From: konradoboza Date: Thu, 2 Apr 2026 10:54:24 +0200 Subject: [PATCH 05/11] fixed wrong condition for supporting `LocationValueView` --- src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php b/src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php index 571f655..54a09b6 100644 --- a/src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php +++ b/src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php @@ -21,7 +21,7 @@ public function __construct(private readonly ResponseTagger $locationTagger) public function supports(mixed $value): bool { - return $value instanceof LocationValueView && !$value->getLocation() instanceof Location; + return $value instanceof LocationValueView && $value->getLocation() instanceof Location; } public function tag(mixed $value) From 3420eec76af23d9fae20e4f9c5feedf620f700ee Mon Sep 17 00:00:00 2001 From: konradoboza Date: Tue, 7 Apr 2026 12:57:08 +0200 Subject: [PATCH 06/11] cr remarks --- .../Delegator/LocationValueViewTagger.php | 5 +- .../ResponseTagger/Value/LocationTagger.php | 2 +- .../Delegator/DispatcherTaggerTest.php | 57 +++++++++++--- .../Value/ContentInfoTaggerTest.php | 24 +++++- .../Value/LocationTaggerTest.php | 77 +++++++++++-------- 5 files changed, 116 insertions(+), 49 deletions(-) diff --git a/src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php b/src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php index 54a09b6..0ef8b3b 100644 --- a/src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php +++ b/src/lib/ResponseTagger/Delegator/LocationValueViewTagger.php @@ -26,9 +26,8 @@ public function supports(mixed $value): bool public function tag(mixed $value) { - /** @var \Ibexa\Core\MVC\Symfony\View\LocationValueView $value */ - $location = $value->getLocation(); + assert($value instanceof LocationValueView); - $this->locationTagger->tag($location); + $this->locationTagger->tag($value->getLocation()); } } diff --git a/src/lib/ResponseTagger/Value/LocationTagger.php b/src/lib/ResponseTagger/Value/LocationTagger.php index 6e7b10a..b7fdc62 100644 --- a/src/lib/ResponseTagger/Value/LocationTagger.php +++ b/src/lib/ResponseTagger/Value/LocationTagger.php @@ -27,7 +27,7 @@ public function tag(mixed $value) $this->responseTagger->addTags([ContentTagInterface::PARENT_LOCATION_PREFIX . $value->parentLocationId]); $this->responseTagger->addTags( array_map( - static function ($pathItem): string { + static function (string $pathItem): string { return ContentTagInterface::PATH_PREFIX . $pathItem; }, $value->getPath() diff --git a/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php b/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php index 368c420..57e75e8 100644 --- a/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php +++ b/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php @@ -27,11 +27,25 @@ public function testCallsTagOnlyOnTaggersThatSupportTheValue(): void $contentInfoTagger = $this->createMock(ContentInfoTagger::class); $locationTagger = $this->createMock(LocationTagger::class); - $contentInfoTagger->method('supports')->with($contentInfo)->willReturn(true); - $locationTagger->method('supports')->with($contentInfo)->willReturn(false); - - $contentInfoTagger->expects(self::once())->method('tag')->with($contentInfo); - $locationTagger->expects(self::never())->method('tag'); + $contentInfoTagger + ->method('supports') + ->with($contentInfo) + ->willReturn(true); + + $locationTagger + ->expects(self::once()) + ->method('supports') + ->with($contentInfo) + ->willReturn(false); + + $contentInfoTagger + ->expects(self::once()) + ->method('tag') + ->with($contentInfo); + + $locationTagger + ->expects(self::never()) + ->method('tag'); $dispatcher = new DispatcherTagger([$contentInfoTagger, $locationTagger]); $dispatcher->tag($contentInfo); @@ -44,11 +58,25 @@ public function testDoesNotCallTagWhenNoTaggerSupportsTheValue(): void $contentInfoTagger = $this->createMock(ContentInfoTagger::class); $locationTagger = $this->createMock(LocationTagger::class); - $contentInfoTagger->method('supports')->with($location)->willReturn(false); - $locationTagger->method('supports')->with($location)->willReturn(false); + $contentInfoTagger + ->expects(self::once()) + ->method('supports') + ->with($location) + ->willReturn(false); + + $locationTagger + ->expects(self::once()) + ->method('supports') + ->with($location) + ->willReturn(false); + + $contentInfoTagger + ->expects(self::never()) + ->method('tag'); - $contentInfoTagger->expects(self::never())->method('tag'); - $locationTagger->expects(self::never())->method('tag'); + $locationTagger + ->expects(self::never()) + ->method('tag'); $dispatcher = new DispatcherTagger([$contentInfoTagger, $locationTagger]); $dispatcher->tag($location); @@ -59,8 +87,15 @@ public function testCustomResponseTaggerImplementationLackingSupportsMethodShoul $foo = new stdClass(); $contentInfoTagger = $this->createMock(ContentInfoTagger::class); - $contentInfoTagger->method('supports')->with($foo)->willReturn(false); - $contentInfoTagger->expects(self::never())->method('tag'); + $contentInfoTagger + ->expects(self::once()) + ->method('supports') + ->with($foo) + ->willReturn(false); + + $contentInfoTagger + ->expects(self::never()) + ->method('tag'); $wasCalled = false; $customTagger = new class($wasCalled) implements ResponseTagger { diff --git a/tests/lib/ResponseTagger/Value/ContentInfoTaggerTest.php b/tests/lib/ResponseTagger/Value/ContentInfoTaggerTest.php index b28a690..063c6c2 100644 --- a/tests/lib/ResponseTagger/Value/ContentInfoTaggerTest.php +++ b/tests/lib/ResponseTagger/Value/ContentInfoTaggerTest.php @@ -56,13 +56,29 @@ public function testTagsWithLocationIdWhenMainLocationIsSet(): void { $value = new ContentInfo(['id' => 1, 'contentTypeId' => 2, 'mainLocationId' => 456]); + $calls = 0; $this->responseTagger ->expects(self::exactly(2)) ->method('addTags') - ->withConsecutive( - [[ContentTagInterface::CONTENT_PREFIX . '1', ContentTagInterface::CONTENT_TYPE_PREFIX . '2']], - [[ContentTagInterface::LOCATION_PREFIX . '456']], - ); + ->willReturnCallback(function (array $tags) use (&$calls): ResponseTagger { + match ($calls++) { + 0 => self::assertSame( + [ + ContentTagInterface::CONTENT_PREFIX . '1', + ContentTagInterface::CONTENT_TYPE_PREFIX . '2', + ], + $tags + ), + 1 => self::assertSame( + [ + ContentTagInterface::LOCATION_PREFIX . '456', + ], + $tags + ), + }; + + return $this->responseTagger; + }); $this->tagger->tag($value); } diff --git a/tests/lib/ResponseTagger/Value/LocationTaggerTest.php b/tests/lib/ResponseTagger/Value/LocationTaggerTest.php index 699fab7..fb76f6e 100644 --- a/tests/lib/ResponseTagger/Value/LocationTaggerTest.php +++ b/tests/lib/ResponseTagger/Value/LocationTaggerTest.php @@ -46,18 +46,23 @@ public function testTagsWithLocationIdWhenNotMainLocation(): void 'contentInfo' => new ContentInfo(['mainLocationId' => 321]), ]); + $calls = 0; $this->responseTagger ->expects(self::exactly(3)) ->method('addTags') - ->withConsecutive( - [[ContentTagInterface::LOCATION_PREFIX . '123']], - [[ContentTagInterface::PARENT_LOCATION_PREFIX . '2']], - [[ - ContentTagInterface::PATH_PREFIX . '1', - ContentTagInterface::PATH_PREFIX . '2', - ContentTagInterface::PATH_PREFIX . '123', - ]], - ); + ->willReturnCallback(function (array $tags) use (&$calls): ResponseTagger { + match ($calls++) { + 0 => self::assertSame([ContentTagInterface::LOCATION_PREFIX . '123'], $tags), + 1 => self::assertSame([ContentTagInterface::PARENT_LOCATION_PREFIX . '2'], $tags), + 2 => self::assertSame([ + ContentTagInterface::PATH_PREFIX . '1', + ContentTagInterface::PATH_PREFIX . '2', + ContentTagInterface::PATH_PREFIX . '123', + ], $tags), + }; + + return $this->responseTagger; + }); $this->tagger->tag($location); } @@ -71,17 +76,22 @@ public function testDoesNotTagLocationIdWhenItIsMainLocation(): void 'contentInfo' => new ContentInfo(['mainLocationId' => 55]), ]); + $calls = 0; $this->responseTagger ->expects(self::exactly(2)) ->method('addTags') - ->withConsecutive( - [[ContentTagInterface::PARENT_LOCATION_PREFIX . '2']], - [[ - ContentTagInterface::PATH_PREFIX . '1', - ContentTagInterface::PATH_PREFIX . '2', - ContentTagInterface::PATH_PREFIX . '55', - ]], - ); + ->willReturnCallback(function (array $tags) use (&$calls): ResponseTagger { + match ($calls++) { + 0 => self::assertSame([ContentTagInterface::PARENT_LOCATION_PREFIX . '2'], $tags), + 1 => self::assertSame([ + ContentTagInterface::PATH_PREFIX . '1', + ContentTagInterface::PATH_PREFIX . '2', + ContentTagInterface::PATH_PREFIX . '55', + ], $tags), + }; + + return $this->responseTagger; + }); $this->tagger->tag($location); } @@ -95,13 +105,17 @@ public function testTagsWithParentLocationId(): void 'contentInfo' => new ContentInfo(['mainLocationId' => null]), ]); + $calls = 0; $this->responseTagger ->expects(self::atLeastOnce()) ->method('addTags') - ->withConsecutive( - [self::anything()], - [[ContentTagInterface::PARENT_LOCATION_PREFIX . '123']], - ); + ->willReturnCallback(function (array $tags) use (&$calls): ResponseTagger { + if ($calls++ === 1) { + self::assertSame([ContentTagInterface::PARENT_LOCATION_PREFIX . '123'], $tags); + } + + return $this->responseTagger; + }); $this->tagger->tag($location); } @@ -115,18 +129,21 @@ public function testTagsWithPathItems(): void 'contentInfo' => new ContentInfo(['mainLocationId' => null]), ]); + $calls = 0; $this->responseTagger ->expects(self::atLeastOnce()) ->method('addTags') - ->withConsecutive( - [self::anything()], - [self::anything()], - [[ - ContentTagInterface::PATH_PREFIX . '1', - ContentTagInterface::PATH_PREFIX . '2', - ContentTagInterface::PATH_PREFIX . '123', - ]], - ); + ->willReturnCallback(function (array $tags) use (&$calls): ResponseTagger { + if ($calls++ === 2) { + self::assertSame([ + ContentTagInterface::PATH_PREFIX . '1', + ContentTagInterface::PATH_PREFIX . '2', + ContentTagInterface::PATH_PREFIX . '123', + ], $tags); + } + + return $this->responseTagger; + }); $this->tagger->tag($location); } From 2744e6dee6530257ad52c6028422342ed47e1b1f Mon Sep 17 00:00:00 2001 From: konradoboza Date: Wed, 8 Apr 2026 11:56:22 +0200 Subject: [PATCH 07/11] cr remarks --- .../ResponseTagger/ResponseTagger.php | 4 +- .../Value/ContentInfoTagger.php | 3 + .../ResponseTagger/Value/LocationTagger.php | 3 + .../Value/ContentInfoTaggerTest.php | 21 ++++--- .../Value/LocationTaggerTest.php | 61 +++++++++++-------- 5 files changed, 57 insertions(+), 35 deletions(-) diff --git a/src/contracts/ResponseTagger/ResponseTagger.php b/src/contracts/ResponseTagger/ResponseTagger.php index 407d0ba..dcda090 100644 --- a/src/contracts/ResponseTagger/ResponseTagger.php +++ b/src/contracts/ResponseTagger/ResponseTagger.php @@ -15,6 +15,8 @@ interface ResponseTagger { /** * Extracts tags from a value. + * + * @param mixed $value */ - public function tag(mixed $value); + public function tag($value); } diff --git a/src/lib/ResponseTagger/Value/ContentInfoTagger.php b/src/lib/ResponseTagger/Value/ContentInfoTagger.php index eae361d..194ecee 100644 --- a/src/lib/ResponseTagger/Value/ContentInfoTagger.php +++ b/src/lib/ResponseTagger/Value/ContentInfoTagger.php @@ -11,6 +11,9 @@ use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo; use Ibexa\Contracts\HttpCache\Handler\ContentTagInterface; +/** + * @final + */ class ContentInfoTagger extends AbstractValueTagger { public function supports(mixed $value): bool diff --git a/src/lib/ResponseTagger/Value/LocationTagger.php b/src/lib/ResponseTagger/Value/LocationTagger.php index b7fdc62..6ccaa21 100644 --- a/src/lib/ResponseTagger/Value/LocationTagger.php +++ b/src/lib/ResponseTagger/Value/LocationTagger.php @@ -11,6 +11,9 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Location; use Ibexa\Contracts\HttpCache\Handler\ContentTagInterface; +/** + * @final + */ class LocationTagger extends AbstractValueTagger { public function supports(mixed $value): bool diff --git a/tests/lib/ResponseTagger/Value/ContentInfoTaggerTest.php b/tests/lib/ResponseTagger/Value/ContentInfoTaggerTest.php index 063c6c2..61ec151 100644 --- a/tests/lib/ResponseTagger/Value/ContentInfoTaggerTest.php +++ b/tests/lib/ResponseTagger/Value/ContentInfoTaggerTest.php @@ -55,27 +55,30 @@ public function testTagsWithContentAndContentTypeId(): void public function testTagsWithLocationIdWhenMainLocationIsSet(): void { $value = new ContentInfo(['id' => 1, 'contentTypeId' => 2, 'mainLocationId' => 456]); + $matcher = self::exactly(2); - $calls = 0; $this->responseTagger - ->expects(self::exactly(2)) + ->expects($matcher) ->method('addTags') - ->willReturnCallback(function (array $tags) use (&$calls): ResponseTagger { - match ($calls++) { - 0 => self::assertSame( + ->willReturnCallback(function (array $tags) use ($matcher): ResponseTagger { + if ($matcher->getInvocationCount() === 1) { + self::assertSame( [ ContentTagInterface::CONTENT_PREFIX . '1', ContentTagInterface::CONTENT_TYPE_PREFIX . '2', ], $tags - ), - 1 => self::assertSame( + ); + } + + if ($matcher->getInvocationCount() === 2) { + self::assertSame( [ ContentTagInterface::LOCATION_PREFIX . '456', ], $tags - ), - }; + ); + } return $this->responseTagger; }); diff --git a/tests/lib/ResponseTagger/Value/LocationTaggerTest.php b/tests/lib/ResponseTagger/Value/LocationTaggerTest.php index fb76f6e..63981cf 100644 --- a/tests/lib/ResponseTagger/Value/LocationTaggerTest.php +++ b/tests/lib/ResponseTagger/Value/LocationTaggerTest.php @@ -46,20 +46,26 @@ public function testTagsWithLocationIdWhenNotMainLocation(): void 'contentInfo' => new ContentInfo(['mainLocationId' => 321]), ]); - $calls = 0; + $matcher = self::exactly(3); $this->responseTagger - ->expects(self::exactly(3)) + ->expects($matcher) ->method('addTags') - ->willReturnCallback(function (array $tags) use (&$calls): ResponseTagger { - match ($calls++) { - 0 => self::assertSame([ContentTagInterface::LOCATION_PREFIX . '123'], $tags), - 1 => self::assertSame([ContentTagInterface::PARENT_LOCATION_PREFIX . '2'], $tags), - 2 => self::assertSame([ + ->willReturnCallback(function (array $tags) use ($matcher): ResponseTagger { + if ($matcher->getInvocationCount() === 1) { + self::assertSame([ContentTagInterface::LOCATION_PREFIX . '123'], $tags); + } + + if ($matcher->getInvocationCount() === 2) { + self::assertSame([ContentTagInterface::PARENT_LOCATION_PREFIX . '2'], $tags); + } + + if ($matcher->getInvocationCount() === 3) { + self::assertSame([ ContentTagInterface::PATH_PREFIX . '1', ContentTagInterface::PATH_PREFIX . '2', ContentTagInterface::PATH_PREFIX . '123', - ], $tags), - }; + ], $tags); + } return $this->responseTagger; }); @@ -76,19 +82,23 @@ public function testDoesNotTagLocationIdWhenItIsMainLocation(): void 'contentInfo' => new ContentInfo(['mainLocationId' => 55]), ]); - $calls = 0; + $matcher = self::exactly(2); + $this->responseTagger - ->expects(self::exactly(2)) + ->expects($matcher) ->method('addTags') - ->willReturnCallback(function (array $tags) use (&$calls): ResponseTagger { - match ($calls++) { - 0 => self::assertSame([ContentTagInterface::PARENT_LOCATION_PREFIX . '2'], $tags), - 1 => self::assertSame([ + ->willReturnCallback(function (array $tags) use ($matcher): ResponseTagger { + if ($matcher->getInvocationCount() === 1) { + self::assertSame([ContentTagInterface::PARENT_LOCATION_PREFIX . '2'], $tags); + } + + if ($matcher->getInvocationCount() === 2) { + self::assertSame([ ContentTagInterface::PATH_PREFIX . '1', ContentTagInterface::PATH_PREFIX . '2', ContentTagInterface::PATH_PREFIX . '55', - ], $tags), - }; + ], $tags); + } return $this->responseTagger; }); @@ -105,12 +115,13 @@ public function testTagsWithParentLocationId(): void 'contentInfo' => new ContentInfo(['mainLocationId' => null]), ]); - $calls = 0; + $matcher = self::atLeastOnce(); + $this->responseTagger - ->expects(self::atLeastOnce()) + ->expects($matcher) ->method('addTags') - ->willReturnCallback(function (array $tags) use (&$calls): ResponseTagger { - if ($calls++ === 1) { + ->willReturnCallback(function (array $tags) use ($matcher): ResponseTagger { + if ($matcher->getInvocationCount() === 0) { self::assertSame([ContentTagInterface::PARENT_LOCATION_PREFIX . '123'], $tags); } @@ -129,12 +140,12 @@ public function testTagsWithPathItems(): void 'contentInfo' => new ContentInfo(['mainLocationId' => null]), ]); - $calls = 0; + $matcher = self::atLeastOnce(); $this->responseTagger - ->expects(self::atLeastOnce()) + ->expects($matcher) ->method('addTags') - ->willReturnCallback(function (array $tags) use (&$calls): ResponseTagger { - if ($calls++ === 2) { + ->willReturnCallback(function (array $tags) use ($matcher): ResponseTagger { + if ($matcher->getInvocationCount() === 0) { self::assertSame([ ContentTagInterface::PATH_PREFIX . '1', ContentTagInterface::PATH_PREFIX . '2', From 1ab3f778117ee21fcb0bd57e115f1580702fa621 Mon Sep 17 00:00:00 2001 From: konradoboza Date: Wed, 8 Apr 2026 12:31:27 +0200 Subject: [PATCH 08/11] cr remarks vol2 --- .../ResponseTagger/Delegator/DispatcherTagger.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/lib/ResponseTagger/Delegator/DispatcherTagger.php b/src/lib/ResponseTagger/Delegator/DispatcherTagger.php index e2ebceb..e16f32c 100644 --- a/src/lib/ResponseTagger/Delegator/DispatcherTagger.php +++ b/src/lib/ResponseTagger/Delegator/DispatcherTagger.php @@ -8,7 +8,6 @@ namespace Ibexa\HttpCache\ResponseTagger\Delegator; use Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger; -use Ibexa\HttpCache\ResponseTagger\Value\AbstractValueTagger; /** * Dispatches a value to all registered ResponseTaggers. @@ -25,9 +24,17 @@ public function __construct(private array $taggers = []) public function tag(mixed $value): void { foreach ($this->taggers as $tagger) { - // AbstractValueTagger subclasses declare supports() and should only tag matching values. - // Custom ResponseTagger implementations lack supports() for BC reasons and are always called. - if (!$tagger instanceof AbstractValueTagger || $tagger->supports($value)) { + if (method_exists($tagger, 'supports')) { + if ($tagger->supports($value)) { + $tagger->tag($value); + } + } else { + trigger_deprecation( + 'ibexa/http-cache', + '5.0.7', + '%s does not implement supports(). This will be required in 6.0, supports() will be a part of ResponseTagger interface', + get_debug_type($tagger), + ); $tagger->tag($value); } } From 26736f7cf6d7add47a9a09d64c870a8e18be686b Mon Sep 17 00:00:00 2001 From: konradoboza Date: Thu, 9 Apr 2026 08:53:24 +0200 Subject: [PATCH 09/11] improved testing and cache-related patch file --- features/setup/symfonyCache.feature | 6 +++--- .../Delegator/ContentValueViewTagger.php | 8 ++++---- .../Delegator/DispatcherTaggerTest.php | 17 ++++++++++++++++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/features/setup/symfonyCache.feature b/features/setup/symfonyCache.feature index 2164b2c..f701c8d 100644 --- a/features/setup/symfonyCache.feature +++ b/features/setup/symfonyCache.feature @@ -19,13 +19,13 @@ index 9982c21..03ac40a 100644 +++ b/public/index.php @@ -1,9 +1,14 @@ getContent(); + assert($value instanceof ContentValueView); - $contentInfo = $content->getVersionInfo()->getContentInfo(); - $this->contentInfoTagger->tag($contentInfo); + $this->contentInfoTagger->tag( + $value->getContent()->getVersionInfo()->getContentInfo() + ); } } diff --git a/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php b/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php index 57e75e8..81e20fc 100644 --- a/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php +++ b/tests/lib/ResponseTagger/Delegator/DispatcherTaggerTest.php @@ -110,9 +110,24 @@ public function tag(mixed $value): void }; $dispatcher = new DispatcherTagger([$contentInfoTagger, $customTagger]); - $dispatcher->tag($foo); + + $deprecation = null; + set_error_handler(static function (int $errorCode, string $errorString) use (&$deprecation): bool { + if ($errorCode === E_USER_DEPRECATED) { + $deprecation = $errorString; + } + + return true; + }); + + try { + $dispatcher->tag($foo); + } finally { + restore_error_handler(); + } self::assertTrue($wasCalled, 'Custom ResponseTagger::tag() was not called by the dispatcher.'); + self::assertStringContainsString('does not implement supports()', $deprecation); } public function testToStringWithNoTaggers(): void From d6402de93624450a6681183b929392ac7d12af3d Mon Sep 17 00:00:00 2001 From: konradoboza Date: Thu, 9 Apr 2026 12:08:37 +0200 Subject: [PATCH 10/11] adjusted cache-patch file to the lates recipes changes from Symfony --- features/setup/symfonyCache.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/setup/symfonyCache.feature b/features/setup/symfonyCache.feature index f701c8d..986933d 100644 --- a/features/setup/symfonyCache.feature +++ b/features/setup/symfonyCache.feature @@ -26,13 +26,13 @@ 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(); + + return new AppCache($kernel); }; --- +-- 2.30.0 """ From e9184b3d69f001829700eaa74375461495198047 Mon Sep 17 00:00:00 2001 From: konradoboza Date: Thu, 9 Apr 2026 12:28:27 +0200 Subject: [PATCH 11/11] moved from array to iterable --- src/lib/ResponseTagger/Delegator/DispatcherTagger.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/ResponseTagger/Delegator/DispatcherTagger.php b/src/lib/ResponseTagger/Delegator/DispatcherTagger.php index e16f32c..39acba5 100644 --- a/src/lib/ResponseTagger/Delegator/DispatcherTagger.php +++ b/src/lib/ResponseTagger/Delegator/DispatcherTagger.php @@ -8,6 +8,7 @@ namespace Ibexa\HttpCache\ResponseTagger\Delegator; use Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger; +use function Ibexa\PolyfillPhp82\iterator_to_array; /** * Dispatches a value to all registered ResponseTaggers. @@ -15,9 +16,9 @@ readonly class DispatcherTagger implements ResponseTagger { /** - * @param \Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger[] $taggers + * @param iterable<\Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger> $taggers */ - public function __construct(private array $taggers = []) + public function __construct(private iterable $taggers = []) { } @@ -46,7 +47,7 @@ public function __toString(): string ', ', array_map( static fn (ResponseTagger $tagger): string => get_debug_type($tagger), - $this->taggers + iterator_to_array($this->taggers) ) );