Skip to content
Open
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
Expand Down
44 changes: 34 additions & 10 deletions config/services.php
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
<?php
<?php declare(strict_types=1);

use FOS\HttpCacheBundle\CacheManager;
use Neusta\Pimcore\HttpCacheBundle\Adapter\FOSHttpCache\CacheInvalidatorAdapter;
use Neusta\Pimcore\HttpCacheBundle\Adapter\FOSHttpCache\ResponseTaggerAdapter;
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\CacheInvalidator\RemoveDisabledTagsCacheInvalidator;
use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTagChecker\Element\AssetCacheTagChecker;
use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTagChecker\Element\DocumentCacheTagChecker;
use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTagChecker\Element\ObjectCacheTagChecker;
use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTagChecker\ElementCacheTagChecker;
use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTagChecker\StaticCacheTagChecker;
use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger;
use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\TraceableResponseTagger;
use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\OnlyWhenActiveResponseTagger;
use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\OnlyWhenEnabledResponseTagger;
use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\RemoveDisabledTagsResponseTagger;
use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\TraceableResponseTagger;
use Neusta\Pimcore\HttpCacheBundle\CacheActivator;
use Neusta\Pimcore\HttpCacheBundle\CacheScope;
use Neusta\Pimcore\HttpCacheBundle\DataCollector;
use Neusta\Pimcore\HttpCacheBundle\Element\ElementRepository;
use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener;
use Neusta\Pimcore\HttpCacheBundle\Element\TagElementListener;
use Neusta\Pimcore\HttpCacheBundle\EventListener\ConsoleCacheScopeListener;
use Neusta\Pimcore\HttpCacheBundle\EventListener\HttpCacheScopeListener;
use Pimcore\Http\Request\Resolver\DocumentResolver;
use Pimcore\Http\Request\Resolver\PimcoreContextResolver;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg;
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
Expand All @@ -28,8 +33,27 @@
return static function (ContainerConfigurator $configurator) {
$services = $configurator->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));
Expand All @@ -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');

Expand All @@ -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)
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions doc/2-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
70 changes: 70 additions & 0 deletions doc/8-cache-scope.md
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
```

Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
27 changes: 21 additions & 6 deletions src/CacheActivator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
61 changes: 61 additions & 0 deletions src/CacheScope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);

namespace Neusta\Pimcore\HttpCacheBundle;

use Symfony\Contracts\Service\ResetInterface;

/**
* @final
*/
class CacheScope implements ResetInterface
{
private bool $enabled = false;
private bool $disabled = false;

/**
* Enables cache-related behavior until the scope is reset.
*
* Has no effect if {@see disable()} was already called.
*/
public function enable(): void
{
$this->enabled = true;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* 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;
}
}
Loading