diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e0cc0..6946dd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog -## Version 1.0.0 +## Version 0.8.0 + +- feature: Introduced `CacheScope` service for controlling when and whether cache tags are collected. Cache invalidation is active by default everywhere (including non-cacheable and admin-context requests) and only stops once `CacheScope::disable()` is called; tag collection instead requires the scope to be explicitly enabled for the current request +- feature: New `scope` configuration option (`controller` / `request`) to control when tag collection begins within a request +- feature: New `tag_fallback_document` configuration option to tag responses with fallback documents +- deprecation: `CacheActivator` is deprecated; use `CacheScope` instead +- deprecation: `CacheActivator::isCachingActive()` is deprecated; use `CacheScope::isInvalidating()` instead +- deprecation: `CacheActivator::activateCaching()` is deprecated; use `CacheScope::enable()` instead +- deprecation: `CacheActivator::deactivateCaching()` is deprecated; use `CacheScope::disable()` instead + +## Version 0.7.0 - feature: Initial Release diff --git a/README.md b/README.md index b2e6395..f899a1d 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,11 @@ You will find the detailed documentation in the following links: * [Additional tags](doc/5-additional-tags.md) * [Cancel caching behavior](doc/6-cancel-caching-behavior.md) * [Custom cache types](doc/7-custom-cache-types.md) -* [Disabling caching behavior](doc/8-disable-caching-behavior.md) -* [Error handling](doc/9-error-handling.md) -* [Contribution](doc/10-contribution.md) +* [Cache scope](doc/8-cache-scope.md) +* [Disabling caching behavior](doc/9-disable-caching-behavior.md) +* [Error handling](doc/10-error-handling.md) +* [Profiling](doc/11-profiling.md) +* [Contribution](doc/12-contribution.md) We hope you will enjoy this bundle as much as we do. If you have any questions or suggestions, please feel free to open an issue on GitHub. This repository is maintained by [neusta](https://www.team-neusta.de/). diff --git a/config/services.php b/config/services.php index 3c06ca5..8786b75 100644 --- a/config/services.php +++ b/config/services.php @@ -1,10 +1,10 @@ -services(); - $services->set('neusta_pimcore_http_cache.cache_activator', CacheActivator::class) - ->alias(CacheActivator::class, 'neusta_pimcore_http_cache.cache_activator'); + $services->set('neusta_pimcore_http_cache.cache_scope', CacheActivator::class) + ->tag('kernel.reset', ['method' => 'reset']) + ->alias(CacheScope::class, 'neusta_pimcore_http_cache.cache_scope'); + + $services->alias('neusta_pimcore_http_cache.cache_activator', 'neusta_pimcore_http_cache.cache_scope') + ->deprecate('teamneusta/pimcore-http-cache-bundle', '0.8', 'The "%alias_id%" alias is deprecated, use "neusta_pimcore_http_cache.cache_scope" instead.') + ->alias(CacheActivator::class, 'neusta_pimcore_http_cache.cache_activator') + ->deprecate('teamneusta/pimcore-http-cache-bundle', '0.8', 'The "%alias_id%" alias is deprecated, use "' . CacheScope::class . '" instead.'); + + $services->set('neusta_pimcore_http_cache.cache_scope.console_listener', ConsoleCacheScopeListener::class) + ->arg('$cacheScope', service('neusta_pimcore_http_cache.cache_scope')) + ->autoconfigure(); + + $services->set('neusta_pimcore_http_cache.cache_scope.http_listener', HttpCacheScopeListener::class) + ->arg('$cacheScope', service('neusta_pimcore_http_cache.cache_scope')) + ->arg('$responseTagger', service('neusta_pimcore_http_cache.response_tagger')) + ->arg('$contextResolver', service(PimcoreContextResolver::class)) + ->arg('$documentResolver', service(DocumentResolver::class)) + ->arg('$collectFromRequest', abstract_arg('Set in the extension')) + ->arg('$tagFallbackDocument', abstract_arg('Set in the extension')) + ->autoconfigure(); $services->set('neusta_pimcore_http_cache.cache_invalidator', CacheInvalidatorAdapter::class) ->arg('$invalidator', service(CacheManager::class)); @@ -38,9 +62,9 @@ ->decorate('neusta_pimcore_http_cache.cache_invalidator', null, -99) ->args([service('.inner'), service('neusta_pimcore_http_cache.cache_tag_checker')]); - $services->set(null, OnlyWhenActiveCacheInvalidator::class) + $services->set(null, OnlyWhenInvalidatingCacheInvalidator::class) ->decorate('neusta_pimcore_http_cache.cache_invalidator', null, -100) - ->args([service('.inner'), service('neusta_pimcore_http_cache.cache_activator')]); + ->args([service('.inner'), service('neusta_pimcore_http_cache.cache_scope')]); $services->alias(CacheInvalidator::class, 'neusta_pimcore_http_cache.cache_invalidator'); @@ -51,9 +75,9 @@ ->decorate('neusta_pimcore_http_cache.response_tagger', null, -99) ->args([service('.inner'), service('neusta_pimcore_http_cache.cache_tag_checker')]); - $services->set(null, OnlyWhenActiveResponseTagger::class) + $services->set(null, OnlyWhenEnabledResponseTagger::class) ->decorate('neusta_pimcore_http_cache.response_tagger', null, -100) - ->args([service('.inner'), service('neusta_pimcore_http_cache.cache_activator')]); + ->args([service('.inner'), service('neusta_pimcore_http_cache.cache_scope')]); $services->set('.neusta_pimcore_http_cache.response_tagger.traceable', TraceableResponseTagger::class) ->decorate('neusta_pimcore_http_cache.response_tagger', null, 100) diff --git a/doc/9-error-handling.md b/doc/10-error-handling.md similarity index 100% rename from doc/9-error-handling.md rename to doc/10-error-handling.md diff --git a/doc/10-profiling.md b/doc/11-profiling.md similarity index 100% rename from doc/10-profiling.md rename to doc/11-profiling.md diff --git a/doc/11-contribution.md b/doc/12-contribution.md similarity index 100% rename from doc/11-contribution.md rename to doc/12-contribution.md diff --git a/doc/2-configuration.md b/doc/2-configuration.md index c8d1cdc..f6c82c6 100644 --- a/doc/2-configuration.md +++ b/doc/2-configuration.md @@ -4,6 +4,14 @@ This bundle is configured via the `neusta_pimcore_http_cache` key in your `confi ```yaml neusta_pimcore_http_cache: + # When to enable the cache scope for collecting cache tags: "controller" (default) or "request" + # See doc/8-cache-scope.md for details + scope: controller + + # Tag the response with a fallback document when the matched route is not a document route + # (i.e. the nearest document resolved by path rather than the route itself) + tag_fallback_document: false + # Enable/disable cache handling for certain element types elements: assets: diff --git a/doc/8-cache-scope.md b/doc/8-cache-scope.md new file mode 100644 index 0000000..898e7ee --- /dev/null +++ b/doc/8-cache-scope.md @@ -0,0 +1,70 @@ +## Cache Scope + +The cache scope controls whether and from which point in the request lifecycle cache-related behavior is enabled. + +Tagging and invalidation are gated independently: + +- **Tagging** only happens while the scope is **enabled**. Tagging only ever makes sense for a request whose response + can actually be cached, so the scope must be explicitly activated (see [Automatic activation](#automatic-activation)). +- **Invalidation** is active by default — everywhere, including non-cacheable requests such as an admin-UI save or a + custom `POST` endpoint — and only stops once `disable()` has been called. Invalidation does not depend on the scope + being enabled. + +Calling `disable()` always stops both tagging and invalidation, regardless of the above. + +### Automatic activation + +The bundle activates the scope automatically in two contexts: + +- **HTTP requests**: The scope is activated for every cacheable request (`GET` or `HEAD`, non-admin context). + When it is activated depends on the [`scope` configuration option](#scope-option). +- **Console commands**: The scope is activated at the start of every console command. + +This only affects tagging; invalidation runs regardless of whether the current request or command activates the scope. + +### Scope option + +The `scope` option controls at which point in the request lifecycle the scope is activated for HTTP requests: + +- `controller` (default): The scope is activated during the `kernel.controller` event. + Elements loaded earlier — e.g. in `kernel.request` event listeners — are **not** tagged. +- `request`: The scope is activated during the `kernel.request` event, i.e. from the very beginning of the request. + Use this if you need to tag elements that are loaded in `kernel.request` listeners. + +```yaml +neusta_pimcore_http_cache: + scope: controller # or "request" +``` + +### Manual access + +You can inject `CacheScope` directly to check or modify the scope state at runtime: + +```php +use Neusta\Pimcore\HttpCacheBundle\CacheScope; + +final class MyService +{ + public function __construct( + private readonly CacheScope $cacheScope, + ) { + } + + public function doSomething(): void + { + if ($this->cacheScope->isEnabled()) { + // tagging is currently enabled + } + + if ($this->cacheScope->isInvalidating()) { + // invalidation is currently active + } + + $this->cacheScope->disable(); + // both tagging and invalidation are disabled from now on + } +} +``` + +To disable cache-related behavior for the current request or command, see +[Disabling caching behavior](9-disable-caching-behavior.md). diff --git a/doc/8-disable-caching-behavior.md b/doc/9-disable-caching-behavior.md similarity index 56% rename from doc/8-disable-caching-behavior.md rename to doc/9-disable-caching-behavior.md index 02771f2..21e1f79 100644 --- a/doc/8-disable-caching-behavior.md +++ b/doc/9-disable-caching-behavior.md @@ -2,7 +2,11 @@ You may encounter situations where you need to disable the default caching behavior for specific requests. This is especially useful for testing purposes. -To achieve this, you can use the `CacheActivator` to disable tagging and invalidation behavior entirely. +To achieve this, you can use the `CacheScope` to disable tagging and invalidation behavior entirely. + +> [!IMPORTANT] +> Once disabled, the cache scope cannot be re-enabled during the same request or command. +> Calling `enable()` afterward has no effect; the disabled state is only cleared when the scope is reset. ### Example for disabling caching behavior in a specific test case ```php @@ -11,11 +15,10 @@ To achieve this, you can use the `CacheActivator` to disable tagging and invalid public function my_awesome_test_case(): void { // Disable the caching behavior - self::getContainer()->get(CacheActivator::class)->deactivateCaching(); + self::getContainer()->get(CacheScope::class)->disable(); // Your test code here self::assertSame('this is amazing!', $result); } ``` - diff --git a/src/Cache/CacheInvalidator/OnlyWhenActiveCacheInvalidator.php b/src/Cache/CacheInvalidator/OnlyWhenInvalidatingCacheInvalidator.php similarity index 64% rename from src/Cache/CacheInvalidator/OnlyWhenActiveCacheInvalidator.php rename to src/Cache/CacheInvalidator/OnlyWhenInvalidatingCacheInvalidator.php index aeba428..3228035 100644 --- a/src/Cache/CacheInvalidator/OnlyWhenActiveCacheInvalidator.php +++ b/src/Cache/CacheInvalidator/OnlyWhenInvalidatingCacheInvalidator.php @@ -4,19 +4,19 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheInvalidator; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTags; -use Neusta\Pimcore\HttpCacheBundle\CacheActivator; +use Neusta\Pimcore\HttpCacheBundle\CacheScope; -final class OnlyWhenActiveCacheInvalidator implements CacheInvalidator +final class OnlyWhenInvalidatingCacheInvalidator implements CacheInvalidator { public function __construct( private readonly CacheInvalidator $inner, - private readonly CacheActivator $cacheActivator, + private readonly CacheScope $cacheScope, ) { } public function invalidate(CacheTags $tags): void { - if ($this->cacheActivator->isCachingActive()) { + if ($this->cacheScope->isInvalidating()) { $this->inner->invalidate($tags); } } diff --git a/src/Cache/ResponseTagger/OnlyWhenActiveResponseTagger.php b/src/Cache/ResponseTagger/OnlyWhenEnabledResponseTagger.php similarity index 64% rename from src/Cache/ResponseTagger/OnlyWhenActiveResponseTagger.php rename to src/Cache/ResponseTagger/OnlyWhenEnabledResponseTagger.php index 249fea4..8b012fa 100644 --- a/src/Cache/ResponseTagger/OnlyWhenActiveResponseTagger.php +++ b/src/Cache/ResponseTagger/OnlyWhenEnabledResponseTagger.php @@ -4,19 +4,19 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTags; use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger; -use Neusta\Pimcore\HttpCacheBundle\CacheActivator; +use Neusta\Pimcore\HttpCacheBundle\CacheScope; -final class OnlyWhenActiveResponseTagger implements ResponseTagger +final class OnlyWhenEnabledResponseTagger implements ResponseTagger { public function __construct( private readonly ResponseTagger $inner, - private readonly CacheActivator $cacheActivator, + private readonly CacheScope $cacheScope, ) { } public function tag(CacheTags $tags): void { - if ($this->cacheActivator->isCachingActive()) { + if ($this->cacheScope->isEnabled()) { $this->inner->tag($tags); } } diff --git a/src/CacheActivator.php b/src/CacheActivator.php index d875317..0cc76f1 100644 --- a/src/CacheActivator.php +++ b/src/CacheActivator.php @@ -2,22 +2,37 @@ namespace Neusta\Pimcore\HttpCacheBundle; -final class CacheActivator +/** + * @deprecated since version 0.8, use {@see CacheScope} instead. + * + * @phpstan-ignore class.extendsFinalByPhpDoc + */ +final class CacheActivator extends CacheScope { - private bool $isCachingActive = true; - public function isCachingActive(): bool { - return $this->isCachingActive; + trigger_deprecation('teamneusta/pimcore-http-cache-bundle', '0.8', '"%s()" is deprecated, use "%s::isInvalidating()" instead.', __METHOD__, CacheScope::class); + + return $this->isInvalidating(); } + /** + * @deprecated Calls {@see CacheScope::reset()} before enabling, which also clears a prior + * {@see CacheScope::disable()} call. Do not mix this deprecated API with the {@see CacheScope} API on + * the same instance; the two do not compose safely. + */ public function activateCaching(): void { - $this->isCachingActive = true; + trigger_deprecation('teamneusta/pimcore-http-cache-bundle', '0.8', '"%s()" is deprecated, use "%s::enable()" instead.', __METHOD__, CacheScope::class); + + $this->reset(); + $this->enable(); } public function deactivateCaching(): void { - $this->isCachingActive = false; + trigger_deprecation('teamneusta/pimcore-http-cache-bundle', '0.8', '"%s()" is deprecated, use "%s::disable()" instead.', __METHOD__, CacheScope::class); + + $this->disable(); } } diff --git a/src/CacheScope.php b/src/CacheScope.php new file mode 100644 index 0000000..4a0f89d --- /dev/null +++ b/src/CacheScope.php @@ -0,0 +1,61 @@ +enabled = true; + } + + /** + * Disables cache-related behavior until the scope is reset. + * + * Survives {@see enable()} calls. + */ + public function disable(): void + { + $this->disabled = true; + } + + /** + * Whether cache-related behavior is currently enabled. + */ + public function isEnabled(): bool + { + return $this->enabled && !$this->disabled; + } + + /** + * Whether cache invalidation is currently active. + * + * Unlike {@see isEnabled()}, invalidation is active by default and only + * stops once {@see disable()} has been called; it does not require an + * explicit {@see enable()} call. + */ + public function isInvalidating(): bool + { + return !$this->disabled; + } + + public function reset(): void + { + $this->enabled = false; + $this->disabled = false; + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 6f93ccb..d6d6908 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -16,7 +16,17 @@ public function getConfigTreeBuilder(): TreeBuilder ->fixXmlConfig('element') ->fixXmlConfig('cache_type') ->children() + ->enumNode('scope') + ->info('When to start collecting cache tags. "controller" (default): only from kernel.controller onwards, elements loaded before (e.g. in kernel.request listeners) are not tagged. "request": collect from the very beginning of the request.') + ->values(['controller', 'request']) + ->defaultValue('controller') + ->end() + ->booleanNode('tag_fallback_document') + ->info('Whether to tag the response with the document found in the request (DynamicRouter::CONTENT_KEY) when it is only a fallback, i.e. it was not resolved by the matched route itself but is merely the nearest document by path. Default: false.') + ->defaultFalse() + ->end() ->arrayNode('elements') + ->info('Controls which element types are tracked.') ->addDefaultsIfNotSet() ->fixXmlConfig('asset') ->fixXmlConfig('document') @@ -30,7 +40,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->arrayNode('types') ->info('Enable/disable cache handling for asset types.') ->beforeNormalization() - ->always(fn ($v) => $v + ['folder' => false]) + ->always(static fn ($v) => $v + ['folder' => false]) ->end() ->normalizeKeys(false) ->useAttributeAsKey('type') @@ -47,7 +57,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->arrayNode('types') ->info('Enable/disable cache handling for document types.') ->beforeNormalization() - ->always(fn ($v) => $v + ['email' => false, 'folder' => false, 'hardlink' => false]) + ->always(static fn ($v) => $v + ['email' => false, 'folder' => false, 'hardlink' => false]) ->end() ->normalizeKeys(false) ->useAttributeAsKey('type') @@ -65,7 +75,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->arrayNode('types') ->info('Enable/disable cache handling for data object types.') ->beforeNormalization() - ->always(fn ($v) => $v + ['folder' => false]) + ->always(static fn ($v) => $v + ['folder' => false]) ->end() ->normalizeKeys(false) ->useAttributeAsKey('type') diff --git a/src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php b/src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php index b1dd629..9124b8d 100644 --- a/src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php +++ b/src/DependencyInjection/NeustaPimcoreHttpCacheExtension.php @@ -24,6 +24,10 @@ public function loadInternal(array $mergedConfig, ContainerBuilder $container): $container->getDefinition('neusta_pimcore_http_cache.cache_tag_checker') ->setArgument('$types', $mergedConfig['cache_types']); + $container->getDefinition('neusta_pimcore_http_cache.cache_scope.http_listener') + ->setArgument('$collectFromRequest', 'request' === ($mergedConfig['scope'] ?? 'controller')) + ->setArgument('$tagFallbackDocument', $mergedConfig['tag_fallback_document'] ?? false); + $this->registerElements($container, $mergedConfig['elements']); } diff --git a/src/EventListener/ConsoleCacheScopeListener.php b/src/EventListener/ConsoleCacheScopeListener.php new file mode 100644 index 0000000..4fbfe07 --- /dev/null +++ b/src/EventListener/ConsoleCacheScopeListener.php @@ -0,0 +1,25 @@ +cacheScope->enable(); + } +} diff --git a/src/EventListener/HttpCacheScopeListener.php b/src/EventListener/HttpCacheScopeListener.php new file mode 100644 index 0000000..6be8be5 --- /dev/null +++ b/src/EventListener/HttpCacheScopeListener.php @@ -0,0 +1,96 @@ +collectFromRequest || !$event->isMainRequest() || !$this->isCacheable($event->getRequest())) { + return; + } + + $this->cacheScope->enable(); + } + + #[AsEventListener] + public function onKernelController(ControllerEvent $event): void + { + if (!$event->isMainRequest()) { + return; + } + + $request = $event->getRequest(); + + if (!$this->collectFromRequest) { + if (!$this->isCacheable($request)) { + return; + } + + $this->cacheScope->enable(); + } + + $route = $request->attributes->get(DynamicRouter::ROUTE_KEY); + $routeElement = match (true) { + $route instanceof DocumentRoute => $route->getDocument(), + $route instanceof DataObjectRoute => $route->getObject(), + default => null, + }; + + if (null !== $routeElement) { + $this->responseTagger->tag(CacheTags::fromElement($routeElement)); + } + + if (!$routeElement instanceof Document && $this->tagFallbackDocument) { + $content = $this->documentResolver->getDocument($request); + + // If the route itself didn't resolve to a Document, the $content can only be a fallback document + // (e.g., the nearest document by path for a custom route), rather than something that the route matched. + if ($content instanceof Document) { + $this->responseTagger->tag(CacheTags::fromElement($content)); + } + } + } + + private function isCacheable(Request $request): bool + { + if (!$request->isMethodCacheable()) { + return false; + } + + if ($this->contextResolver->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN)) { + return false; + } + + return true; + } +} diff --git a/tests/Integration/Configuration/CollectConfigurationDataTest.php b/tests/Integration/Configuration/CollectConfigurationDataTest.php index 222e8a7..2cfad8a 100644 --- a/tests/Integration/Configuration/CollectConfigurationDataTest.php +++ b/tests/Integration/Configuration/CollectConfigurationDataTest.php @@ -3,7 +3,6 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Configuration; use Neusta\Pimcore\HttpCacheBundle\DataCollector; -use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\ArrangeCacheTest; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestDocumentFactory; use Neusta\Pimcore\TestingFramework\Database\ResetDatabase; use Neusta\Pimcore\TestingFramework\Test\Attribute\ConfigureExtension; @@ -19,7 +18,6 @@ #[ConfigureRoute(__DIR__ . '/../Fixtures/get_document_route.php')] final class CollectConfigurationDataTest extends ConfigurableWebTestcase { - use ArrangeCacheTest; use ResetDatabase; private KernelBrowser $client; @@ -40,7 +38,7 @@ protected function setUp(): void ])] public function collects_configuration_data(): void { - self::arrange(static fn () => TestDocumentFactory::simplePage())->save(); + TestDocumentFactory::simplePage()->save(); $this->client->request('GET', '/test_document_page'); $this->client->enableProfiler(); @@ -65,7 +63,7 @@ public function collects_configuration_data(): void ])] public function does_not_collect_configuration_data_when_profiler_is_disabled(): void { - self::arrange(static fn () => TestDocumentFactory::simplePage())->save(); + TestDocumentFactory::simplePage()->save(); $this->client->request('GET', '/test_document_page'); $this->client->enableProfiler(); diff --git a/tests/Integration/Fixtures/update_object_route.php b/tests/Integration/Fixtures/update_object_route.php new file mode 100644 index 0000000..e01668c --- /dev/null +++ b/tests/Integration/Fixtures/update_object_route.php @@ -0,0 +1,10 @@ +add('update_object', '/update-object') + ->controller(UpdateObjectController::class) + ->methods(['POST']); +}; diff --git a/tests/Integration/Helpers/ArrangeCacheTest.php b/tests/Integration/Helpers/ArrangeCacheTest.php index bedd907..fd1c5e4 100644 --- a/tests/Integration/Helpers/ArrangeCacheTest.php +++ b/tests/Integration/Helpers/ArrangeCacheTest.php @@ -2,12 +2,12 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers; -use Neusta\Pimcore\HttpCacheBundle\CacheActivator; +use Neusta\Pimcore\HttpCacheBundle\CacheScope; trait ArrangeCacheTest { /** - * Lets you prepare the prerequisites for your test without interfering with the caching. + * Lets you prepare the prerequisites for your test without triggering tagging or invalidation. * * @template T * @@ -17,15 +17,14 @@ trait ArrangeCacheTest */ public static function arrange(\Closure $arrange): mixed { - $cacheActivator = self::getContainer()->get('test.cache_activator'); - \assert($cacheActivator instanceof CacheActivator); + $cacheScope = self::getContainer()->get('neusta_pimcore_http_cache.cache_scope'); + \assert($cacheScope instanceof CacheScope); - $wasActive = $cacheActivator->isCachingActive(); - $cacheActivator->deactivateCaching(); + $cacheScope->disable(); try { return $arrange(); } finally { - $wasActive && $cacheActivator->activateCaching(); + $cacheScope->reset(); } } } diff --git a/tests/Integration/Invalidation/InvalidateAssetTest.php b/tests/Integration/Invalidation/InvalidateAssetTest.php index 486af18..f1efd82 100644 --- a/tests/Integration/Invalidation/InvalidateAssetTest.php +++ b/tests/Integration/Invalidation/InvalidateAssetTest.php @@ -34,9 +34,11 @@ protected function setUp(): void $this->cacheManager->invalidateTags(Argument::any())->willReturn($this->cacheManager->reveal()); self::getContainer()->set('fos_http_cache.cache_manager', $this->cacheManager->reveal()); - $this->asset = self::arrange(static fn () => TestAssetFactory::simpleAsset()->save()); - $this->folder = self::arrange(static fn () => TestAssetFactory::simpleFolder()->save()); - $this->image = self::arrange(static fn () => TestAssetFactory::simpleImage()->save()); + self::arrange(function (): void { + $this->asset = TestAssetFactory::simpleAsset()->save(); + $this->folder = TestAssetFactory::simpleFolder()->save(); + $this->image = TestAssetFactory::simpleImage()->save(); + }); } /** diff --git a/tests/Integration/Invalidation/InvalidateDocumentTest.php b/tests/Integration/Invalidation/InvalidateDocumentTest.php index 4586097..0653570 100644 --- a/tests/Integration/Invalidation/InvalidateDocumentTest.php +++ b/tests/Integration/Invalidation/InvalidateDocumentTest.php @@ -36,10 +36,12 @@ protected function setUp(): void $this->cacheManager->invalidateTags(Argument::any())->willReturn($this->cacheManager->reveal()); self::getContainer()->set('fos_http_cache.cache_manager', $this->cacheManager->reveal()); - $this->document = self::arrange(static fn () => TestDocumentFactory::simplePage()->save()); - $this->hardlink = self::arrange(static fn () => TestDocumentFactory::simpleHardLink()->save()); - $this->email = self::arrange(static fn () => TestDocumentFactory::simpleEmail()->save()); - $this->folder = self::arrange(static fn () => TestDocumentFactory::simpleFolder()->save()); + self::arrange(function (): void { + $this->document = TestDocumentFactory::simplePage()->save(); + $this->hardlink = TestDocumentFactory::simpleHardLink()->save(); + $this->email = TestDocumentFactory::simpleEmail()->save(); + $this->folder = TestDocumentFactory::simpleFolder()->save(); + }); } /** diff --git a/tests/Integration/Invalidation/InvalidateFromNonCacheableRequestTest.php b/tests/Integration/Invalidation/InvalidateFromNonCacheableRequestTest.php new file mode 100644 index 0000000..17e6155 --- /dev/null +++ b/tests/Integration/Invalidation/InvalidateFromNonCacheableRequestTest.php @@ -0,0 +1,61 @@ + */ + private ObjectProphecy $cacheManager; + + protected function setUp(): void + { + $this->client = self::createClient(); + + $this->cacheManager = $this->prophesize(CacheManager::class); + $this->cacheManager->invalidateTags(Argument::any())->willReturn($this->cacheManager->reveal()); + $this->cacheManager->flush()->willReturn(0); + self::getContainer()->set('fos_http_cache.cache_manager', $this->cacheManager->reveal()); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'elements' => [ + 'objects' => true, + ], + ])] + public function response_is_invalidated_when_object_is_updated_via_a_post_request(): void + { + self::arrange(static fn () => TestObjectFactory::simpleObject()->save()); + + $this->client->request('POST', '/update-object?id=42', ['content' => 'Updated test content']); + + self::assertSame(200, $this->client->getResponse()->getStatusCode()); + $this->cacheManager->invalidateTags(['o42'])->shouldHaveBeenCalledTimes(1); + } +} diff --git a/tests/Integration/Invalidation/InvalidateObjectTest.php b/tests/Integration/Invalidation/InvalidateObjectTest.php index b553d66..6e2b2b9 100644 --- a/tests/Integration/Invalidation/InvalidateObjectTest.php +++ b/tests/Integration/Invalidation/InvalidateObjectTest.php @@ -35,9 +35,11 @@ protected function setUp(): void $this->cacheManager->invalidateTags(Argument::any())->willReturn($this->cacheManager->reveal()); self::getContainer()->set('fos_http_cache.cache_manager', $this->cacheManager->reveal()); - $this->object = self::arrange(static fn () => TestObjectFactory::simpleObject()->save()); - $this->folder = self::arrange(static fn () => TestObjectFactory::simpleFolder()->save()); - $this->variant = self::arrange(static fn () => TestObjectFactory::simpleVariant()->save()); + self::arrange(function (): void { + $this->object = TestObjectFactory::simpleObject()->save(); + $this->folder = TestObjectFactory::simpleFolder()->save(); + $this->variant = TestObjectFactory::simpleVariant()->save(); + }); } /** diff --git a/tests/Integration/Tagging/CollectTagsDataTest.php b/tests/Integration/Tagging/CollectTagsDataTest.php index 1434735..0ebbed0 100644 --- a/tests/Integration/Tagging/CollectTagsDataTest.php +++ b/tests/Integration/Tagging/CollectTagsDataTest.php @@ -4,7 +4,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheType\CustomCacheType; -use Neusta\Pimcore\HttpCacheBundle\CacheActivator; +use Neusta\Pimcore\HttpCacheBundle\CacheScope; use Neusta\Pimcore\HttpCacheBundle\DataCollector; use Neusta\Pimcore\HttpCacheBundle\Element\ElementTaggingEvent; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\ArrangeCacheTest; @@ -46,7 +46,7 @@ protected function setUp(): void #[ConfigureRoute(__DIR__ . '/../Fixtures/get_document_route.php')] public function collect_tags_for_type_document(): void { - self::arrange(static fn () => TestDocumentFactory::simplePage())->save(); + self::arrange(static fn () => TestDocumentFactory::simplePage()->save()); $this->client->request('GET', '/test_document_page'); $this->client->enableProfiler(); @@ -55,7 +55,38 @@ public function collect_tags_for_type_document(): void self::assertInstanceOf(DataCollector::class, $dataCollector); self::assertSame( - [['tag' => 'd1', 'type' => 'document'], ['tag' => 'd42', 'type' => 'document']], + [['tag' => 'd42', 'type' => 'document']], + $dataCollector->getTags(), + ); + } + + /** + * @test + */ + #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'scope' => 'request', + 'elements' => [ + 'documents' => true, + ], + ])] + #[ConfigureRoute(__DIR__ . '/../Fixtures/get_document_route.php')] + public function collect_tags_for_type_document_in_request_scope(): void + { + $parent = self::arrange(static fn () => TestDocumentFactory::simplePage()->save()); + self::arrange(static fn () => TestDocumentFactory::simpleSnippet()->setParent($parent)->save()); + + $this->client->request('GET', '/test_document_page/test_document_snippet'); + $this->client->enableProfiler(); + + $dataCollector = $this->client->getProfile()->getCollector('pimcore_http_cache'); + + self::assertInstanceOf(DataCollector::class, $dataCollector); + self::assertSame( + [ + ['tag' => 'd23', 'type' => 'document'], // The document itself + ['tag' => 'd42', 'type' => 'document'], // The document's parent + ['tag' => 'd1', 'type' => 'document'], // The document's parent's parent + ], $dataCollector->getTags(), ); } @@ -179,7 +210,7 @@ public function does_not_collect_tags_when_type_is_disabled(): void public function does_not_collect_tags_when_caching_is_disabled(): void { self::arrange(static fn () => TestObjectFactory::simpleObject()->save()); - self::getContainer()->get(CacheActivator::class)->deactivateCaching(); + self::getContainer()->get(CacheScope::class)->disable(); $this->client->request('GET', '/get-object?id=42'); $this->client->enableProfiler(); diff --git a/tests/Integration/Tagging/TagAdditionalTagTest.php b/tests/Integration/Tagging/TagAdditionalTagTest.php index 0a7fc0f..c387b15 100644 --- a/tests/Integration/Tagging/TagAdditionalTagTest.php +++ b/tests/Integration/Tagging/TagAdditionalTagTest.php @@ -42,8 +42,10 @@ protected function setUp(): void ])] public function response_is_tagged_with_additional_tag_when_asset_is_loaded(): void { - self::arrange(static fn () => TestAssetFactory::simpleAsset()->save()); - self::arrange(static fn () => TestAssetFactory::simpleImage()->save()); + self::arrange(static function (): void { + TestAssetFactory::simpleAsset()->save(); + TestAssetFactory::simpleImage()->save(); + }); self::getContainer()->get('event_dispatcher')->addListener( ElementTaggingEvent::class, @@ -72,8 +74,10 @@ public function response_is_tagged_with_additional_tag_when_asset_is_loaded(): v ])] public function response_is_tagged_with_additional_tag_when_document_is_loaded(): void { - self::arrange(static fn () => TestDocumentFactory::simplePage()->save()); - self::arrange(static fn () => TestDocumentFactory::simpleSnippet()->save()); + self::arrange(static function (): void { + TestDocumentFactory::simplePage()->save(); + TestDocumentFactory::simpleSnippet()->save(); + }); self::getContainer()->get('event_dispatcher')->addListener( ElementTaggingEvent::class, @@ -82,7 +86,7 @@ public function response_is_tagged_with_additional_tag_when_document_is_loaded() ), ); - $this->client->request('GET', '/test_document_page'); + $this->client->request('GET', '/get-document?id=42'); $response = $this->client->getResponse(); self::assertSame('Document with key: test_document_page', $response->getContent()); @@ -102,8 +106,10 @@ public function response_is_tagged_with_additional_tag_when_document_is_loaded() ])] public function response_is_tagged_with_additional_tag_when_object_is_loaded(): void { - self::arrange(static fn () => TestObjectFactory::simpleObject()->save()); - self::arrange(static fn () => TestObjectFactory::simpleVariant()->save()); + self::arrange(static function (): void { + TestObjectFactory::simpleObject()->save(); + TestObjectFactory::simpleVariant()->save(); + }); self::getContainer()->get('event_dispatcher')->addListener( ElementTaggingEvent::class, diff --git a/tests/Integration/Tagging/TagAssetTest.php b/tests/Integration/Tagging/TagAssetTest.php index f44a946..fa345e3 100644 --- a/tests/Integration/Tagging/TagAssetTest.php +++ b/tests/Integration/Tagging/TagAssetTest.php @@ -2,7 +2,7 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Tagging; -use Neusta\Pimcore\HttpCacheBundle\CacheActivator; +use Neusta\Pimcore\HttpCacheBundle\CacheScope; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\ArrangeCacheTest; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestAssetFactory; use Neusta\Pimcore\TestingFramework\Database\ResetDatabase; @@ -79,7 +79,7 @@ public function response_is_not_tagged_when_assets_is_not_enabled(): void public function response_is_not_tagged_when_caching_is_deactivated(): void { self::arrange(static fn () => TestAssetFactory::simpleAsset()->save()); - self::getContainer()->get(CacheActivator::class)->deactivateCaching(); + self::getContainer()->get(CacheScope::class)->disable(); $this->client->request('GET', '/get-asset?id=42'); diff --git a/tests/Integration/Tagging/TagDocumentTest.php b/tests/Integration/Tagging/TagDocumentTest.php index 10b464a..67f4e3d 100644 --- a/tests/Integration/Tagging/TagDocumentTest.php +++ b/tests/Integration/Tagging/TagDocumentTest.php @@ -2,7 +2,7 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Tagging; -use Neusta\Pimcore\HttpCacheBundle\CacheActivator; +use Neusta\Pimcore\HttpCacheBundle\CacheScope; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\ArrangeCacheTest; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestDocumentFactory; use Neusta\Pimcore\TestingFramework\Database\ResetDatabase; @@ -87,7 +87,7 @@ public function response_is_not_tagged_when_document_type_is_email(): void self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringNotContainsString('d17', $response->headers->get('X-Cache-Tags')); + self::assertStringNotContainsString('d17', $response->headers->get('X-Cache-Tags', '')); } /** @@ -109,7 +109,7 @@ public function response_is_not_tagged_when_document_type_is_hard_link(): void self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringNotContainsString('d33', $response->headers->get('X-Cache-Tags')); + self::assertStringNotContainsString('d33', $response->headers->get('X-Cache-Tags', '')); } /** @@ -131,7 +131,7 @@ public function response_is_not_tagged_when_document_type_is_folder(): void self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringNotContainsString('d97', $response->headers->get('X-Cache-Tags')); + self::assertStringNotContainsString('d97', $response->headers->get('X-Cache-Tags', '')); } /** @@ -167,7 +167,7 @@ public function response_is_not_tagged_when_documents_is_not_enabled(): void public function response_is_not_tagged_when_caching_is_deactivated(): void { self::arrange(static fn () => TestDocumentFactory::simplePage()->save()); - self::getContainer()->get(CacheActivator::class)->deactivateCaching(); + self::getContainer()->get(CacheScope::class)->disable(); $this->client->request('GET', '/test_document_page'); @@ -183,22 +183,26 @@ public function response_is_not_tagged_when_caching_is_deactivated(): void * @test */ #[ConfigureExtension('neusta_pimcore_http_cache', [ + 'scope' => 'request', 'elements' => [ 'documents' => true, ], ])] - public function response_is_tagged_with_root_document_tag_when_loaded(): void + public function response_is_tagged_with_the_document_parents_when_in_request_scope(): void { - self::arrange(static fn () => TestDocumentFactory::simplePage()->save()); + $parent = self::arrange(static fn () => TestDocumentFactory::simplePage()->save()); + self::arrange(static fn () => TestDocumentFactory::simpleSnippet()->setParent($parent)->save()); - $this->client->request('GET', '/test_document_page'); + $this->client->request('GET', '/test_document_page/test_document_snippet'); $response = $this->client->getResponse(); - self::assertSame('Document with key: test_document_page', $response->getContent()); + self::assertSame('Document with key: test_document_snippet', $response->getContent()); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->getCacheControlDirective('public')); self::assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); - self::assertStringContainsString('d1', $response->headers->get('X-Cache-Tags')); + self::assertStringContainsString('d23', $response->headers->get('X-Cache-Tags')); // The document itself + self::assertStringContainsString('d42', $response->headers->get('X-Cache-Tags')); // The document's parent + self::assertStringContainsString('d1', $response->headers->get('X-Cache-Tags')); // The document's parent's parent } /** diff --git a/tests/Integration/Tagging/TagObjectTest.php b/tests/Integration/Tagging/TagObjectTest.php index 039a573..92d7c64 100644 --- a/tests/Integration/Tagging/TagObjectTest.php +++ b/tests/Integration/Tagging/TagObjectTest.php @@ -2,7 +2,7 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Tagging; -use Neusta\Pimcore\HttpCacheBundle\CacheActivator; +use Neusta\Pimcore\HttpCacheBundle\CacheScope; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\ArrangeCacheTest; use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestObjectFactory; use Neusta\Pimcore\TestingFramework\Database\ResetDatabase; @@ -79,7 +79,7 @@ public function response_is_not_tagged_when_objects_is_not_enabled(): void public function response_is_not_tagged_when_caching_is_deactivated(): void { self::arrange(static fn () => TestObjectFactory::simpleObject()->save()); - self::getContainer()->get(CacheActivator::class)->deactivateCaching(); + self::getContainer()->get(CacheScope::class)->disable(); $this->client->request('GET', '/get-object?id=42'); diff --git a/tests/Unit/Cache/CacheInvalidator/OnlyWhenActiveCacheInvalidatorTest.php b/tests/Unit/Cache/CacheInvalidator/OnlyWhenInvalidatingCacheInvalidatorTest.php similarity index 60% rename from tests/Unit/Cache/CacheInvalidator/OnlyWhenActiveCacheInvalidatorTest.php rename to tests/Unit/Cache/CacheInvalidator/OnlyWhenInvalidatingCacheInvalidatorTest.php index 9f9de17..bc66a28 100644 --- a/tests/Unit/Cache/CacheInvalidator/OnlyWhenActiveCacheInvalidatorTest.php +++ b/tests/Unit/Cache/CacheInvalidator/OnlyWhenInvalidatingCacheInvalidatorTest.php @@ -3,44 +3,44 @@ namespace Neusta\Pimcore\HttpCacheBundle\Tests\Unit\Cache\CacheInvalidator; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheInvalidator; -use Neusta\Pimcore\HttpCacheBundle\Cache\CacheInvalidator\OnlyWhenActiveCacheInvalidator; +use Neusta\Pimcore\HttpCacheBundle\Cache\CacheInvalidator\OnlyWhenInvalidatingCacheInvalidator; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTags; -use Neusta\Pimcore\HttpCacheBundle\CacheActivator; +use Neusta\Pimcore\HttpCacheBundle\CacheScope; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -final class OnlyWhenActiveCacheInvalidatorTest extends TestCase +final class OnlyWhenInvalidatingCacheInvalidatorTest extends TestCase { use ProphecyTrait; - private OnlyWhenActiveCacheInvalidator $subject; + private OnlyWhenInvalidatingCacheInvalidator $subject; /** @var ObjectProphecy */ private ObjectProphecy $decorated; - /** @var ObjectProphecy */ - private ObjectProphecy $cacheActivator; + /** @var ObjectProphecy */ + private ObjectProphecy $cacheScope; protected function setUp(): void { $this->decorated = $this->prophesize(CacheInvalidator::class); - $this->cacheActivator = $this->prophesize(CacheActivator::class); - $this->subject = new OnlyWhenActiveCacheInvalidator( + $this->cacheScope = $this->prophesize(CacheScope::class); + $this->subject = new OnlyWhenInvalidatingCacheInvalidator( $this->decorated->reveal(), - $this->cacheActivator->reveal(), + $this->cacheScope->reveal(), ); } /** * @test */ - public function it_should_invalidate_tags_when_caching_is_active(): void + public function it_should_invalidate_tags_when_invalidation_is_active(): void { $tags = CacheTags::fromStrings(['tag1', 'tag2']); - $this->cacheActivator->isCachingActive()->willReturn(true); + $this->cacheScope->isInvalidating()->willReturn(true); $this->subject->invalidate($tags); @@ -50,11 +50,11 @@ public function it_should_invalidate_tags_when_caching_is_active(): void /** * @test */ - public function it_should_not_invalidate_tags_when_caching_is_not_active(): void + public function it_should_not_invalidate_tags_when_invalidation_is_not_active(): void { $tags = CacheTags::fromStrings(['tag1', 'tag2']); - $this->cacheActivator->isCachingActive()->willReturn(false); + $this->cacheScope->isInvalidating()->willReturn(false); $this->subject->invalidate($tags); diff --git a/tests/Unit/Cache/ResponseTagger/OnlyWhenActiveResponseTaggerTest.php b/tests/Unit/Cache/ResponseTagger/OnlyWhenEnabledResponseTaggerTest.php similarity index 68% rename from tests/Unit/Cache/ResponseTagger/OnlyWhenActiveResponseTaggerTest.php rename to tests/Unit/Cache/ResponseTagger/OnlyWhenEnabledResponseTaggerTest.php index 3512462..1d1ce08 100644 --- a/tests/Unit/Cache/ResponseTagger/OnlyWhenActiveResponseTaggerTest.php +++ b/tests/Unit/Cache/ResponseTagger/OnlyWhenEnabledResponseTaggerTest.php @@ -4,32 +4,32 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTags; use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger; -use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\OnlyWhenActiveResponseTagger; -use Neusta\Pimcore\HttpCacheBundle\CacheActivator; +use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\OnlyWhenEnabledResponseTagger; +use Neusta\Pimcore\HttpCacheBundle\CacheScope; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -final class OnlyWhenActiveResponseTaggerTest extends TestCase +final class OnlyWhenEnabledResponseTaggerTest extends TestCase { use ProphecyTrait; - private OnlyWhenActiveResponseTagger $subject; + private OnlyWhenEnabledResponseTagger $subject; /** @var ObjectProphecy */ private ObjectProphecy $decorated; - /** @var ObjectProphecy */ - private ObjectProphecy $cacheActivator; + /** @var ObjectProphecy */ + private ObjectProphecy $cacheScope; protected function setUp(): void { $this->decorated = $this->prophesize(ResponseTagger::class); - $this->cacheActivator = $this->prophesize(CacheActivator::class); - $this->subject = new OnlyWhenActiveResponseTagger( + $this->cacheScope = $this->prophesize(CacheScope::class); + $this->subject = new OnlyWhenEnabledResponseTagger( $this->decorated->reveal(), - $this->cacheActivator->reveal(), + $this->cacheScope->reveal(), ); } @@ -40,7 +40,7 @@ public function it_should_invalidate_tags_when_caching_is_active(): void { $tags = CacheTags::fromStrings(['tag1', 'tag2']); - $this->cacheActivator->isCachingActive()->willReturn(true); + $this->cacheScope->isEnabled()->willReturn(true); $this->subject->tag($tags); @@ -54,7 +54,7 @@ public function it_should_not_invalidate_tags_when_caching_is_not_active(): void { $tags = CacheTags::fromStrings(['tag1', 'tag2']); - $this->cacheActivator->isCachingActive()->willReturn(false); + $this->cacheScope->isEnabled()->willReturn(false); $this->subject->tag($tags); diff --git a/tests/Unit/CacheActivatorTest.php b/tests/Unit/CacheActivatorTest.php index c0084e5..256cb3d 100644 --- a/tests/Unit/CacheActivatorTest.php +++ b/tests/Unit/CacheActivatorTest.php @@ -17,16 +17,27 @@ protected function setUp(): void /** * @test */ - public function it_must_be_activated_by_default(): void + public function it_is_caching_active_by_default(): void { self::assertTrue($this->cacheActivator->isCachingActive()); } + /** + * @test + */ + public function it_must_be_activated_after_activateCaching_is_called(): void + { + $this->cacheActivator->activateCaching(); + + self::assertTrue($this->cacheActivator->isCachingActive()); + } + /** * @test */ public function it_must_be_deactivated_after_deactivateCaching_is_called(): void { + $this->cacheActivator->activateCaching(); $this->cacheActivator->deactivateCaching(); self::assertFalse($this->cacheActivator->isCachingActive()); @@ -35,9 +46,12 @@ public function it_must_be_deactivated_after_deactivateCaching_is_called(): void /** * @test */ - public function it_must_be_activated_after_activateCaching_is_called(): void + public function it_can_be_deactivated_and_activated_again(): void { $this->cacheActivator->deactivateCaching(); + + self::assertFalse($this->cacheActivator->isCachingActive()); + $this->cacheActivator->activateCaching(); self::assertTrue($this->cacheActivator->isCachingActive()); diff --git a/tests/Unit/CacheScopeTest.php b/tests/Unit/CacheScopeTest.php new file mode 100644 index 0000000..d4b43af --- /dev/null +++ b/tests/Unit/CacheScopeTest.php @@ -0,0 +1,130 @@ +isEnabled()); + } + + /** + * @test + */ + public function enable_enables_the_scope(): void + { + $cacheScope = new CacheScope(); + + $cacheScope->enable(); + + self::assertTrue($cacheScope->isEnabled()); + } + + /** + * @test + */ + public function disable_disables_scope_and_collection(): void + { + $cacheScope = new CacheScope(); + $cacheScope->enable(); + + $cacheScope->disable(); + + self::assertFalse($cacheScope->isEnabled()); + } + + /** + * @test + */ + public function disable_survives_later_enable_calls(): void + { + $cacheScope = new CacheScope(); + $cacheScope->disable(); + + $cacheScope->enable(); + + self::assertFalse($cacheScope->isEnabled()); + } + + /** + * @test + */ + public function reset_deactivates_the_scope(): void + { + $cacheScope = new CacheScope(); + $cacheScope->enable(); + + $cacheScope->reset(); + + self::assertFalse($cacheScope->isEnabled()); + } + + /** + * @test + */ + public function reset_clears_disabled_state_so_scope_can_be_enabled_again(): void + { + $cacheScope = new CacheScope(); + $cacheScope->disable(); + + $cacheScope->reset(); + $cacheScope->enable(); + + self::assertTrue($cacheScope->isEnabled()); + } + + /** + * @test + */ + public function it_is_invalidating_by_default(): void + { + $cacheScope = new CacheScope(); + + self::assertTrue($cacheScope->isInvalidating()); + } + + /** + * @test + */ + public function it_is_invalidating_even_when_not_explicitly_enabled(): void + { + $cacheScope = new CacheScope(); + $cacheScope->enable(); + + self::assertTrue($cacheScope->isInvalidating()); + } + + /** + * @test + */ + public function disable_stops_invalidation(): void + { + $cacheScope = new CacheScope(); + + $cacheScope->disable(); + + self::assertFalse($cacheScope->isInvalidating()); + } + + /** + * @test + */ + public function reset_clears_disabled_state_so_invalidation_resumes(): void + { + $cacheScope = new CacheScope(); + $cacheScope->disable(); + + $cacheScope->reset(); + + self::assertTrue($cacheScope->isInvalidating()); + } +} diff --git a/tests/Unit/EventListener/HttpCacheScopeListenerTest.php b/tests/Unit/EventListener/HttpCacheScopeListenerTest.php new file mode 100644 index 0000000..09f7f22 --- /dev/null +++ b/tests/Unit/EventListener/HttpCacheScopeListenerTest.php @@ -0,0 +1,425 @@ + */ + private ObjectProphecy $cacheScope; + + /** @var ObjectProphecy */ + private ObjectProphecy $responseTagger; + + /** @var ObjectProphecy */ + private ObjectProphecy $contextResolver; + + /** @var ObjectProphecy */ + private ObjectProphecy $documentResolver; + + /** @var ObjectProphecy */ + private ObjectProphecy $kernel; + + protected function setUp(): void + { + $this->cacheScope = $this->prophesize(CacheScope::class); + $this->responseTagger = $this->prophesize(ResponseTagger::class); + $this->contextResolver = $this->prophesize(PimcoreContextResolver::class); + $this->documentResolver = $this->prophesize(DocumentResolver::class); + $this->kernel = $this->prophesize(HttpKernelInterface::class); + } + + /** + * @test + */ + public function onKernelRequest_does_not_activate_caching_when_collecting_from_request_is_disabled(): void + { + $subject = $this->createSubject(collectFromRequest: false); + + $subject->onKernelRequest($this->createRequestEvent(new Request())); + + $this->cacheScope->enable()->shouldNotHaveBeenCalled(); + $this->contextResolver->matchesPimcoreContext(Argument::cetera())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelRequest_does_not_activate_caching_for_sub_request(): void + { + $subject = $this->createSubject(collectFromRequest: true); + + $subject->onKernelRequest($this->createRequestEvent(new Request(), HttpKernelInterface::SUB_REQUEST)); + + $this->cacheScope->enable()->shouldNotHaveBeenCalled(); + $this->contextResolver->matchesPimcoreContext(Argument::cetera())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelRequest_does_not_activate_caching_for_non_cacheable_method(): void + { + $subject = $this->createSubject(collectFromRequest: true); + + $subject->onKernelRequest($this->createRequestEvent(Request::create('/', 'POST'))); + + $this->cacheScope->enable()->shouldNotHaveBeenCalled(); + $this->contextResolver->matchesPimcoreContext(Argument::cetera())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelRequest_does_not_activate_caching_for_admin_context(): void + { + $subject = $this->createSubject(collectFromRequest: true); + $request = new Request(); + + $this->contextResolver + ->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN) + ->willReturn(true); + + $subject->onKernelRequest($this->createRequestEvent($request)); + + $this->cacheScope->enable()->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelRequest_activates_caching_for_cacheable_main_request_outside_admin_context(): void + { + $subject = $this->createSubject(collectFromRequest: true); + $request = new Request(); + + $this->contextResolver + ->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN) + ->willReturn(false); + + $subject->onKernelRequest($this->createRequestEvent($request)); + + $this->cacheScope->enable()->shouldHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelController_does_nothing_for_sub_request(): void + { + $subject = $this->createSubject(); + + $subject->onKernelController($this->createControllerEvent(new Request(), HttpKernelInterface::SUB_REQUEST)); + + $this->cacheScope->enable()->shouldNotHaveBeenCalled(); + $this->responseTagger->tag(Argument::any())->shouldNotHaveBeenCalled(); + $this->documentResolver->getDocument(Argument::any())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelController_does_not_activate_or_tag_for_non_cacheable_method_when_collecting_from_controller(): void + { + $subject = $this->createSubject(collectFromRequest: false); + + $subject->onKernelController($this->createControllerEvent(Request::create('/', 'POST'))); + + $this->cacheScope->enable()->shouldNotHaveBeenCalled(); + $this->responseTagger->tag(Argument::any())->shouldNotHaveBeenCalled(); + $this->documentResolver->getDocument(Argument::any())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelController_does_not_activate_or_tag_for_admin_context_when_collecting_from_controller(): void + { + $subject = $this->createSubject(collectFromRequest: false); + $request = new Request(); + + $this->contextResolver + ->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN) + ->willReturn(true); + + $subject->onKernelController($this->createControllerEvent($request)); + + $this->cacheScope->enable()->shouldNotHaveBeenCalled(); + $this->responseTagger->tag(Argument::any())->shouldNotHaveBeenCalled(); + $this->documentResolver->getDocument(Argument::any())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelController_activates_caching_when_collecting_from_controller(): void + { + $subject = $this->createSubject(collectFromRequest: false); + $request = new Request(); + + $this->contextResolver + ->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN) + ->willReturn(false); + + $this->documentResolver + ->getDocument($request) + ->willReturn(null); + + $subject->onKernelController($this->createControllerEvent($request)); + + $this->cacheScope->enable()->shouldHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelController_does_not_activate_caching_when_already_collecting_from_request(): void + { + $subject = $this->createSubject(collectFromRequest: true); + $request = new Request(); + + $this->documentResolver + ->getDocument($request) + ->willReturn(null); + + $subject->onKernelController($this->createControllerEvent($request)); + + $this->contextResolver->matchesPimcoreContext(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->cacheScope->enable()->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelController_tags_document_from_document_route(): void + { + $subject = $this->createSubject(); + $document = $this->createDocument(123); + $route = $this->prophesize(DocumentRoute::class); + $request = new Request(); + + $request->attributes->set(DynamicRouter::ROUTE_KEY, $route->reveal()); + + $this->contextResolver + ->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN) + ->willReturn(false); + + $route + ->getDocument() + ->willReturn($document); + + $subject->onKernelController($this->createControllerEvent($request)); + + $this->responseTagger + ->tag(Argument::that(static fn (CacheTags $tags): bool => ['d123'] === $tags->toArray())) + ->shouldHaveBeenCalled(); + + $this->documentResolver->getDocument(Argument::any())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelController_tags_object_from_data_object_route(): void + { + $subject = $this->createSubject(); + $object = $this->createDataObject(456); + $route = $this->prophesize(DataObjectRoute::class); + $request = new Request(); + + $request->attributes->set(DynamicRouter::ROUTE_KEY, $route->reveal()); + + $this->contextResolver + ->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN) + ->willReturn(false); + + $route + ->getObject() + ->willReturn($object); + + $this->documentResolver + ->getDocument($request) + ->willReturn(null); + + $subject->onKernelController($this->createControllerEvent($request)); + + $this->responseTagger + ->tag(Argument::that(static fn (CacheTags $tags): bool => ['o456'] === $tags->toArray())) + ->shouldHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelController_does_not_tag_fallback_document_when_disabled(): void + { + $subject = $this->createSubject(tagFallbackDocument: false); + $object = $this->createDataObject(456); + $route = $this->prophesize(DataObjectRoute::class); + $request = new Request(); + + $request->attributes->set(DynamicRouter::ROUTE_KEY, $route->reveal()); + + $this->contextResolver + ->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN) + ->willReturn(false); + + $route + ->getObject() + ->willReturn($object); + + $subject->onKernelController($this->createControllerEvent($request)); + + $this->responseTagger + ->tag(Argument::that(static fn (CacheTags $tags): bool => ['o456'] === $tags->toArray())) + ->shouldHaveBeenCalled(); + + $this->documentResolver->getDocument(Argument::any())->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelController_tags_fallback_document_when_route_element_is_not_document(): void + { + $subject = $this->createSubject(tagFallbackDocument: true); + $object = $this->createDataObject(456); + $fallbackDocument = $this->createDocument(789); + $route = $this->prophesize(DataObjectRoute::class); + $request = new Request(); + + $request->attributes->set(DynamicRouter::ROUTE_KEY, $route->reveal()); + + $this->contextResolver + ->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN) + ->willReturn(false); + + $route + ->getObject() + ->willReturn($object); + + $this->documentResolver + ->getDocument($request) + ->willReturn($fallbackDocument); + + $subject->onKernelController($this->createControllerEvent($request)); + + $this->responseTagger + ->tag(Argument::that(static fn (CacheTags $tags): bool => ['o456'] === $tags->toArray())) + ->shouldHaveBeenCalled(); + + $this->responseTagger + ->tag(Argument::that(static fn (CacheTags $tags): bool => ['d789'] === $tags->toArray())) + ->shouldHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelController_tags_only_fallback_document_when_route_is_unknown(): void + { + $subject = $this->createSubject(tagFallbackDocument: true); + $fallbackDocument = $this->createDocument(789); + $request = new Request(); + + $this->contextResolver + ->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN) + ->willReturn(false); + + $this->documentResolver + ->getDocument($request) + ->willReturn($fallbackDocument); + + $subject->onKernelController($this->createControllerEvent($request)); + + $this->responseTagger + ->tag(Argument::that(static fn (CacheTags $tags): bool => ['d789'] === $tags->toArray())) + ->shouldHaveBeenCalled(); + } + + /** + * @test + */ + public function onKernelController_does_not_tag_anything_when_route_is_unknown_and_no_fallback_document_exists(): void + { + $subject = $this->createSubject(tagFallbackDocument: true); + $request = new Request(); + + $this->contextResolver + ->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN) + ->willReturn(false); + + $this->documentResolver + ->getDocument($request) + ->willReturn(null); + + $subject->onKernelController($this->createControllerEvent($request)); + + $this->responseTagger->tag(Argument::any())->shouldNotHaveBeenCalled(); + } + + private function createSubject( + bool $collectFromRequest = false, + bool $tagFallbackDocument = false, + ): HttpCacheScopeListener { + return new HttpCacheScopeListener( + $this->cacheScope->reveal(), + $this->responseTagger->reveal(), + $this->contextResolver->reveal(), + $this->documentResolver->reveal(), + $collectFromRequest, + $tagFallbackDocument, + ); + } + + private function createRequestEvent( + Request $request, + int $requestType = HttpKernelInterface::MAIN_REQUEST, + ): RequestEvent { + return new RequestEvent($this->kernel->reveal(), $request, $requestType); + } + + private function createControllerEvent( + Request $request, + int $requestType = HttpKernelInterface::MAIN_REQUEST, + ): ControllerEvent { + return new ControllerEvent($this->kernel->reveal(), static fn () => null, $request, $requestType); + } + + private function createDocument(int $id): Document + { + $document = $this->prophesize(Document::class); + $document->getId()->willReturn($id); + + return $document->reveal(); + } + + private function createDataObject(int $id): Concrete + { + $object = $this->prophesize(Concrete::class); + $object->getId()->willReturn($id); + + return $object->reveal(); + } +} diff --git a/tests/app/config/services.php b/tests/app/config/services.php index 76ef43d..df2f8ba 100644 --- a/tests/app/config/services.php +++ b/tests/app/config/services.php @@ -23,7 +23,4 @@ ->tag('kernel.event_listener', ['event' => DataObjectEvents::POST_DELETE]) ->tag('kernel.event_listener', ['event' => DocumentEvents::POST_UPDATE]) ->tag('kernel.event_listener', ['event' => DocumentEvents::POST_DELETE]); - - $services->alias('test.cache_activator', 'neusta_pimcore_http_cache.cache_activator') - ->public(); }; diff --git a/tests/app/src/Controller/UpdateObjectController.php b/tests/app/src/Controller/UpdateObjectController.php new file mode 100644 index 0000000..b879f3c --- /dev/null +++ b/tests/app/src/Controller/UpdateObjectController.php @@ -0,0 +1,21 @@ +query->getInt('id'))) { + return new Response('Object not found', Response::HTTP_NOT_FOUND); + } + + $object->setContent($request->request->getString('content'))->save(); + + return new Response('Object updated'); + } +}