diff --git a/CHANGELOG.md b/CHANGELOG.md index 6946dd8..972f59e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - 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 +- feature: New `CacheScope::withoutCollecting()`/`withCollecting()` methods to temporarily pause/resume tag collection for a block of code, and `isCollecting()` to check the current state +- feature: New `CacheScope::withoutInvalidating()`/`withInvalidating()` methods to temporarily pause/resume cache invalidation for a block of code, e.g. around a bulk import - 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 diff --git a/config/services.php b/config/services.php index 8786b75..a383798 100644 --- a/config/services.php +++ b/config/services.php @@ -12,7 +12,7 @@ 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\OnlyWhenEnabledResponseTagger; +use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\OnlyWhenCollectingResponseTagger; use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\RemoveDisabledTagsResponseTagger; use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\TraceableResponseTagger; use Neusta\Pimcore\HttpCacheBundle\CacheActivator; @@ -75,7 +75,7 @@ ->decorate('neusta_pimcore_http_cache.response_tagger', null, -99) ->args([service('.inner'), service('neusta_pimcore_http_cache.cache_tag_checker')]); - $services->set(null, OnlyWhenEnabledResponseTagger::class) + $services->set(null, OnlyWhenCollectingResponseTagger::class) ->decorate('neusta_pimcore_http_cache.response_tagger', null, -100) ->args([service('.inner'), service('neusta_pimcore_http_cache.cache_scope')]); diff --git a/doc/8-cache-scope.md b/doc/8-cache-scope.md index 898e7ee..7b9bcc6 100644 --- a/doc/8-cache-scope.md +++ b/doc/8-cache-scope.md @@ -4,11 +4,14 @@ The cache scope controls whether and from which point in the request lifecycle c 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)). +- **Tagging** only happens while the scope is **enabled** and collection isn't paused (see + [Temporarily pausing collection](#temporarily-pausing-collection)). 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. + custom `POST` endpoint — and only stops once `disable()` has been called, or while temporarily paused via + `withoutInvalidating()` (see [Temporarily pausing invalidation](#temporarily-pausing-invalidation)). Invalidation + does not depend on the scope being enabled or on tag collection being paused. Calling `disable()` always stops both tagging and invalidation, regardless of the above. @@ -46,14 +49,14 @@ use Neusta\Pimcore\HttpCacheBundle\CacheScope; final class MyService { public function __construct( - private readonly CacheScope $cacheScope, + private CacheScope $cacheScope, ) { } public function doSomething(): void { - if ($this->cacheScope->isEnabled()) { - // tagging is currently enabled + if ($this->cacheScope->isCollecting()) { + // tagging is currently active } if ($this->cacheScope->isInvalidating()) { @@ -66,5 +69,166 @@ final class MyService } ``` +### Temporarily pausing collection + +Use `withoutCollecting()` to temporarily pause tag collection for a specific block of code. + +> [!NOTE] +> `withoutCollecting()` only pauses response tag collection. +> It does not disable cache invalidation. +> Use `disable()` if you want to disable cache-related behavior entirely. + +```php +use Neusta\Pimcore\HttpCacheBundle\CacheScope; + +final class MyService +{ + public function __construct( + private CacheScope $cacheScope, + ) { + } + + public function doSomething(): mixed + { + return $this->cacheScope->withoutCollecting(function (CacheScope $scope): mixed { + // cache tags are not collected here + + return $this->loadSomething(); + }); + } + + private function loadSomething(): mixed + { + // ... + } +} +``` + +### Temporarily resuming collection + +Use `withCollecting()` to temporarily enable collection within a callback. +This can be useful inside `withoutCollecting()` when only a smaller inner section should collect cache tags again. + +> [!NOTE] +> `withCollecting()` still respects `disable()`. +> If the scope was disabled for the current request or command, `withCollecting()` will not make the scope enabled. + +```php +use Neusta\Pimcore\HttpCacheBundle\CacheScope; + +final class MyService +{ + public function __construct( + private readonly CacheScope $cacheScope, + ) { + } + + public function doSomething(): mixed + { + return $this->cacheScope->withoutCollecting(function (CacheScope $scope): mixed { + // cache tags are not collected here + + $result = $scope->withCollecting(function (): mixed { + // cache tags are collected here again, + // unless the scope was disabled for the request or command + + return $this->loadSomethingThatShouldBeTagged(); + }); + + // cache tags are not collected here again + + return $result; + }); + } + + private function loadSomethingThatShouldBeTagged(): mixed + { + // ... + } +} +``` + +### Temporarily pausing invalidation + +Use `withoutInvalidating()` to temporarily pause cache invalidation for a specific block of code, e.g. around a bulk +import that saves many elements you don't want to trigger invalidation for. + +> [!NOTE] +> `withoutInvalidating()` only pauses cache invalidation. +> It does not disable response tagging. +> Use `disable()` if you want to disable cache-related behavior entirely. + +```php +use Neusta\Pimcore\HttpCacheBundle\CacheScope; + +final class MyService +{ + public function __construct( + private CacheScope $cacheScope, + ) { + } + + public function doSomething(): mixed + { + return $this->cacheScope->withoutInvalidating(function (CacheScope $scope): mixed { + // cache invalidation does not happen here + + return $this->importElements(); + }); + } + + private function importElements(): mixed + { + // ... + } +} +``` + +### Temporarily resuming invalidation + +Use `withInvalidating()` to temporarily enable invalidation within a callback. +This can be useful inside `withoutInvalidating()` when only a smaller inner section should invalidate again. + +> [!NOTE] +> `withInvalidating()` still respects `disable()`. +> If the scope was disabled for the current request or command, `withInvalidating()` will not make invalidation active. + +```php +use Neusta\Pimcore\HttpCacheBundle\CacheScope; + +final class MyService +{ + public function __construct( + private readonly CacheScope $cacheScope, + ) { + } + + public function doSomething(): mixed + { + return $this->cacheScope->withoutInvalidating(function (CacheScope $scope): mixed { + // cache invalidation does not happen here + + $result = $scope->withInvalidating(function (): mixed { + // cache invalidation happens here again, + // unless the scope was disabled for the request or command + + return $this->saveElementThatShouldInvalidate(); + }); + + // cache invalidation does not happen here again + + return $result; + }); + } + + private function saveElementThatShouldInvalidate(): mixed + { + // ... + } +} +``` + +### Disabling the scope + To disable cache-related behavior for the current request or command, see [Disabling caching behavior](9-disable-caching-behavior.md). diff --git a/doc/9-disable-caching-behavior.md b/doc/9-disable-caching-behavior.md index 21e1f79..2f95150 100644 --- a/doc/9-disable-caching-behavior.md +++ b/doc/9-disable-caching-behavior.md @@ -1,24 +1,37 @@ ## Disable caching behavior -You may encounter situations where you need to disable the default caching behavior for specific requests. +You may encounter situations where you need to disable the default caching behavior for specific requests or commands. This is especially useful for testing purposes. -To achieve this, you can use the `CacheScope` to disable tagging and invalidation behavior entirely. + +To achieve this, you can use `CacheScope::disable()` to disable tagging and invalidation behavior entirely +for the current request or command. > [!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. +> Once disabled, the cache scope cannot be re-enabled by calling `enable()` or `withCollecting()`/`withInvalidating()` during the same request or command. +> The disabled state is only cleared when the scope is reset. ### Example for disabling caching behavior in a specific test case + ```php +use Neusta\Pimcore\HttpCacheBundle\CacheScope; +use Pimcore\Test\KernelTestCase; - /** @test */ - public function my_awesome_test_case(): void +final class MyAwesomeTest extends KernelTestCase +{ + /** @test */ + public function my_awesome_test_case(): void { - // Disable the caching behavior self::getContainer()->get(CacheScope::class)->disable(); // Your test code here self::assertSame('this is amazing!', $result); } +} ``` + +### Difference to temporarily pausing collection or invalidation + +`disable()` disables cache-related behavior for the whole request or command. +If you only want to temporarily pause or resume tag collection or invalidation for a specific block of code, use the +cache scope methods described in [Cache Scope](8-cache-scope.md#temporarily-pausing-collection) instead. diff --git a/src/Cache/ResponseTagger/OnlyWhenEnabledResponseTagger.php b/src/Cache/ResponseTagger/OnlyWhenCollectingResponseTagger.php similarity index 80% rename from src/Cache/ResponseTagger/OnlyWhenEnabledResponseTagger.php rename to src/Cache/ResponseTagger/OnlyWhenCollectingResponseTagger.php index 8b012fa..97502d1 100644 --- a/src/Cache/ResponseTagger/OnlyWhenEnabledResponseTagger.php +++ b/src/Cache/ResponseTagger/OnlyWhenCollectingResponseTagger.php @@ -6,7 +6,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger; use Neusta\Pimcore\HttpCacheBundle\CacheScope; -final class OnlyWhenEnabledResponseTagger implements ResponseTagger +final class OnlyWhenCollectingResponseTagger implements ResponseTagger { public function __construct( private readonly ResponseTagger $inner, @@ -16,7 +16,7 @@ public function __construct( public function tag(CacheTags $tags): void { - if ($this->cacheScope->isEnabled()) { + if ($this->cacheScope->isCollecting()) { $this->inner->tag($tags); } } diff --git a/src/CacheScope.php b/src/CacheScope.php index 4a0f89d..3ba57fd 100644 --- a/src/CacheScope.php +++ b/src/CacheScope.php @@ -12,6 +12,8 @@ class CacheScope implements ResetInterface { private bool $enabled = false; private bool $disabled = false; + private bool $collectionPaused = false; + private bool $invalidationPaused = false; /** * Enables cache-related behavior until the scope is reset. @@ -34,28 +36,125 @@ public function disable(): void } /** - * Whether cache-related behavior is currently enabled. + * Whether response tag collection is currently enabled and not paused. */ - public function isEnabled(): bool + public function isCollecting(): bool { - return $this->enabled && !$this->disabled; + return $this->enabled && !$this->disabled && !$this->collectionPaused; + } + + /** + * Executes the callable with tag collection temporarily paused. + * + * @template T + * + * @param (\Closure(self): T) $fn + * + * @return T + */ + public function withoutCollecting(\Closure $fn): mixed + { + $previousCollectionPaused = $this->collectionPaused; + $this->collectionPaused = true; + + try { + return $fn($this); + } finally { + $this->collectionPaused = $previousCollectionPaused; + } + } + + /** + * Executes the callable with tag collection enabled. + * + * Temporarily resumes collection inside {@see withoutCollecting()}, but still respects {@see disable()}. + * + * @template T + * + * @param (\Closure(self): T) $fn + * + * @return T + */ + public function withCollecting(\Closure $fn): mixed + { + $previousEnabled = $this->enabled; + $previousCollectionPaused = $this->collectionPaused; + + $this->enabled = true; + $this->collectionPaused = false; + + try { + return $fn($this); + } finally { + $this->enabled = $previousEnabled; + $this->collectionPaused = $previousCollectionPaused; + } } /** * Whether cache invalidation is currently active. * - * Unlike {@see isEnabled()}, invalidation is active by default and only + * Unlike {@see isCollecting()}, invalidation is active by default and only * stops once {@see disable()} has been called; it does not require an - * explicit {@see enable()} call. + * explicit {@see enable()} call. It may still be paused via {@see withoutInvalidating()}. */ public function isInvalidating(): bool { - return !$this->disabled; + return !$this->disabled && !$this->invalidationPaused; + } + + /** + * Executes the callable with cache invalidation temporarily paused. + * + * Useful for suppressing invalidation around code that saves elements you don't want + * to trigger cache invalidation, e.g. bulk imports. + * + * @template T + * + * @param (\Closure(self): T) $fn + * + * @return T + */ + public function withoutInvalidating(\Closure $fn): mixed + { + $previousInvalidationPaused = $this->invalidationPaused; + $this->invalidationPaused = true; + + try { + return $fn($this); + } finally { + $this->invalidationPaused = $previousInvalidationPaused; + } + } + + /** + * Executes the callable with cache invalidation enabled. + * + * Temporarily resumes invalidation inside {@see withoutInvalidating()}, but still respects {@see disable()}. + * + * @template T + * + * @param (\Closure(self): T) $fn + * + * @return T + */ + public function withInvalidating(\Closure $fn): mixed + { + $previousInvalidationPaused = $this->invalidationPaused; + $this->invalidationPaused = false; + + try { + return $fn($this); + } finally { + $this->invalidationPaused = $previousInvalidationPaused; + } } public function reset(): void { $this->enabled = false; $this->disabled = false; + $this->collectionPaused = false; + $this->invalidationPaused = false; } } diff --git a/tests/Unit/Cache/CacheInvalidator/OnlyWhenInvalidatingCacheInvalidatorTest.php b/tests/Unit/Cache/CacheInvalidator/OnlyWhenInvalidatingCacheInvalidatorTest.php index bc66a28..a1aefa1 100644 --- a/tests/Unit/Cache/CacheInvalidator/OnlyWhenInvalidatingCacheInvalidatorTest.php +++ b/tests/Unit/Cache/CacheInvalidator/OnlyWhenInvalidatingCacheInvalidatorTest.php @@ -60,4 +60,45 @@ public function it_should_not_invalidate_tags_when_invalidation_is_not_active(): $this->decorated->invalidate(Argument::any())->shouldNotHaveBeenCalled(); } + + /** + * @test + */ + public function it_should_invalidate_tags_when_collection_is_paused(): void + { + $tags = CacheTags::fromStrings(['tag1', 'tag2']); + $cacheScope = new CacheScope(); + $cacheScope->enable(); + + $subject = new OnlyWhenInvalidatingCacheInvalidator( + $this->decorated->reveal(), + $cacheScope, + ); + + $cacheScope->withoutCollecting(static function () use ($subject, $tags): void { + $subject->invalidate($tags); + }); + + $this->decorated->invalidate($tags)->shouldHaveBeenCalled(); + } + + /** + * @test + */ + public function it_should_not_invalidate_tags_when_invalidation_is_paused(): void + { + $tags = CacheTags::fromStrings(['tag1', 'tag2']); + $cacheScope = new CacheScope(); + + $subject = new OnlyWhenInvalidatingCacheInvalidator( + $this->decorated->reveal(), + $cacheScope, + ); + + $cacheScope->withoutInvalidating(static function () use ($subject, $tags): void { + $subject->invalidate($tags); + }); + + $this->decorated->invalidate(Argument::any())->shouldNotHaveBeenCalled(); + } } diff --git a/tests/Unit/Cache/ResponseTagger/OnlyWhenEnabledResponseTaggerTest.php b/tests/Unit/Cache/ResponseTagger/OnlyWhenCollectingResponseTaggerTest.php similarity index 72% rename from tests/Unit/Cache/ResponseTagger/OnlyWhenEnabledResponseTaggerTest.php rename to tests/Unit/Cache/ResponseTagger/OnlyWhenCollectingResponseTaggerTest.php index 1d1ce08..f0289c8 100644 --- a/tests/Unit/Cache/ResponseTagger/OnlyWhenEnabledResponseTaggerTest.php +++ b/tests/Unit/Cache/ResponseTagger/OnlyWhenCollectingResponseTaggerTest.php @@ -4,18 +4,18 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTags; use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger; -use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\OnlyWhenEnabledResponseTagger; +use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger\OnlyWhenCollectingResponseTagger; use Neusta\Pimcore\HttpCacheBundle\CacheScope; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -final class OnlyWhenEnabledResponseTaggerTest extends TestCase +final class OnlyWhenCollectingResponseTaggerTest extends TestCase { use ProphecyTrait; - private OnlyWhenEnabledResponseTagger $subject; + private OnlyWhenCollectingResponseTagger $subject; /** @var ObjectProphecy */ private ObjectProphecy $decorated; @@ -27,7 +27,7 @@ protected function setUp(): void { $this->decorated = $this->prophesize(ResponseTagger::class); $this->cacheScope = $this->prophesize(CacheScope::class); - $this->subject = new OnlyWhenEnabledResponseTagger( + $this->subject = new OnlyWhenCollectingResponseTagger( $this->decorated->reveal(), $this->cacheScope->reveal(), ); @@ -36,11 +36,11 @@ protected function setUp(): void /** * @test */ - public function it_should_invalidate_tags_when_caching_is_active(): void + public function it_should_invalidate_tags_when_scope_is_collecting(): void { $tags = CacheTags::fromStrings(['tag1', 'tag2']); - $this->cacheScope->isEnabled()->willReturn(true); + $this->cacheScope->isCollecting()->willReturn(true); $this->subject->tag($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_scope_is_not_collecting(): void { $tags = CacheTags::fromStrings(['tag1', 'tag2']); - $this->cacheScope->isEnabled()->willReturn(false); + $this->cacheScope->isCollecting()->willReturn(false); $this->subject->tag($tags); diff --git a/tests/Unit/CacheScopeTest.php b/tests/Unit/CacheScopeTest.php index d4b43af..d5c6923 100644 --- a/tests/Unit/CacheScopeTest.php +++ b/tests/Unit/CacheScopeTest.php @@ -14,7 +14,7 @@ public function it_is_disabled_by_default(): void { $cacheScope = new CacheScope(); - self::assertFalse($cacheScope->isEnabled()); + self::assertFalse($cacheScope->isCollecting()); } /** @@ -26,7 +26,7 @@ public function enable_enables_the_scope(): void $cacheScope->enable(); - self::assertTrue($cacheScope->isEnabled()); + self::assertTrue($cacheScope->isCollecting()); } /** @@ -39,7 +39,7 @@ public function disable_disables_scope_and_collection(): void $cacheScope->disable(); - self::assertFalse($cacheScope->isEnabled()); + self::assertFalse($cacheScope->isCollecting()); } /** @@ -52,20 +52,147 @@ public function disable_survives_later_enable_calls(): void $cacheScope->enable(); - self::assertFalse($cacheScope->isEnabled()); + self::assertFalse($cacheScope->isCollecting()); } /** * @test */ - public function reset_deactivates_the_scope(): void + public function withoutCollecting_stops_collection(): void + { + $cacheScope = new CacheScope(); + $cacheScope->enable(); + + $cacheScope->withoutCollecting(static function (CacheScope $scope): void { + self::assertFalse($scope->isCollecting()); + }); + + self::assertTrue($cacheScope->isCollecting()); + } + + /** + * @test + */ + public function withoutCollecting_returns_the_closure_result(): void + { + $cacheScope = new CacheScope(); + + $result = $cacheScope->withoutCollecting(static fn () => 'result'); + + self::assertSame('result', $result); + } + + /** + * @test + */ + public function nested_withoutCollecting_keeps_collection_paused_until_outer_scope_finishes(): void + { + $cacheScope = new CacheScope(); + $cacheScope->enable(); + + $cacheScope->withoutCollecting(static function (CacheScope $scope): void { + self::assertFalse($scope->isCollecting()); + + $scope->withoutCollecting(static function (CacheScope $scope): void { + self::assertFalse($scope->isCollecting()); + }); + + self::assertFalse($scope->isCollecting()); + }); + + self::assertTrue($cacheScope->isCollecting()); + } + + /** + * @test + */ + public function withCollecting_temporarily_enables_the_scope(): void + { + $cacheScope = new CacheScope(); + + self::assertFalse($cacheScope->isCollecting()); + + $cacheScope->withCollecting(static function (CacheScope $scope): void { + self::assertTrue($scope->isCollecting()); + }); + + self::assertFalse($cacheScope->isCollecting()); + } + + /** + * @test + */ + public function withCollecting_returns_the_closure_result(): void + { + $cacheScope = new CacheScope(); + + $result = $cacheScope->withCollecting(static fn () => 'result'); + + self::assertSame('result', $result); + } + + /** + * @test + */ + public function withCollecting_respects_disable(): void + { + $cacheScope = new CacheScope(); + $cacheScope->disable(); + + $cacheScope->withCollecting(static function (CacheScope $scope): void { + self::assertFalse($scope->isCollecting()); + }); + + self::assertFalse($cacheScope->isCollecting()); + } + + /** + * @test + */ + public function withCollecting_temporarily_resumes_collection_inside_withoutCollecting(): void + { + $cacheScope = new CacheScope(); + $cacheScope->enable(); + + $cacheScope->withoutCollecting(static function (CacheScope $scope): void { + self::assertFalse($scope->isCollecting()); + + $scope->withCollecting(static function (CacheScope $scope): void { + self::assertTrue($scope->isCollecting()); + }); + + self::assertFalse($scope->isCollecting()); + }); + + self::assertTrue($cacheScope->isCollecting()); + } + + /** + * @test + */ + public function withCollecting_restores_previous_state_after_closure(): void + { + $cacheScope = new CacheScope(); + $cacheScope->enable(); + + $cacheScope->withCollecting(static function (CacheScope $scope): void { + self::assertTrue($scope->isCollecting()); + }); + + self::assertTrue($cacheScope->isCollecting()); + } + + /** + * @test + */ + public function reset_disables_the_scope(): void { $cacheScope = new CacheScope(); $cacheScope->enable(); $cacheScope->reset(); - self::assertFalse($cacheScope->isEnabled()); + self::assertFalse($cacheScope->isCollecting()); } /** @@ -79,7 +206,7 @@ public function reset_clears_disabled_state_so_scope_can_be_enabled_again(): voi $cacheScope->reset(); $cacheScope->enable(); - self::assertTrue($cacheScope->isEnabled()); + self::assertTrue($cacheScope->isCollecting()); } /** @@ -127,4 +254,97 @@ public function reset_clears_disabled_state_so_invalidation_resumes(): void self::assertTrue($cacheScope->isInvalidating()); } + + /** + * @test + */ + public function withoutInvalidating_stops_invalidation(): void + { + $cacheScope = new CacheScope(); + + $cacheScope->withoutInvalidating(static function (CacheScope $scope): void { + self::assertFalse($scope->isInvalidating()); + }); + + self::assertTrue($cacheScope->isInvalidating()); + } + + /** + * @test + */ + public function withoutInvalidating_returns_the_closure_result(): void + { + $cacheScope = new CacheScope(); + + $result = $cacheScope->withoutInvalidating(static fn () => 'result'); + + self::assertSame('result', $result); + } + + /** + * @test + */ + public function nested_withoutInvalidating_keeps_invalidation_paused_until_outer_scope_finishes(): void + { + $cacheScope = new CacheScope(); + + $cacheScope->withoutInvalidating(static function (CacheScope $scope): void { + self::assertFalse($scope->isInvalidating()); + + $scope->withoutInvalidating(static function (CacheScope $scope): void { + self::assertFalse($scope->isInvalidating()); + }); + + self::assertFalse($scope->isInvalidating()); + }); + + self::assertTrue($cacheScope->isInvalidating()); + } + + /** + * @test + */ + public function withInvalidating_returns_the_closure_result(): void + { + $cacheScope = new CacheScope(); + + $result = $cacheScope->withInvalidating(static fn () => 'result'); + + self::assertSame('result', $result); + } + + /** + * @test + */ + public function withInvalidating_respects_disable(): void + { + $cacheScope = new CacheScope(); + $cacheScope->disable(); + + $cacheScope->withInvalidating(static function (CacheScope $scope): void { + self::assertFalse($scope->isInvalidating()); + }); + + self::assertFalse($cacheScope->isInvalidating()); + } + + /** + * @test + */ + public function withInvalidating_temporarily_resumes_invalidation_inside_withoutInvalidating(): void + { + $cacheScope = new CacheScope(); + + $cacheScope->withoutInvalidating(static function (CacheScope $scope): void { + self::assertFalse($scope->isInvalidating()); + + $scope->withInvalidating(static function (CacheScope $scope): void { + self::assertTrue($scope->isInvalidating()); + }); + + self::assertFalse($scope->isInvalidating()); + }); + + self::assertTrue($cacheScope->isInvalidating()); + } }