Repository: spatie/laravel-responsecache
Pull request #495: Introduce `ClearingResponseCacheFailed` and return boolean by `clear()`
Author: login: alies-dev
Created: 2025-05-15T19:35:48Z | Merged: 2025-05-20T08:39:05Z

## Overview

So far, when you can `ResponseCache::clear()`, it returns void and emits 2 events:

https://github.com/spatie/laravel-responsecache/blob/f77f2ae14d5f054704c9daafd9b8dcd4cb6bdf4a/src/ResponseCache.php#L87-L94

Down the callstack, `$this->taggedCache($tags)->clear();`  will call [`\Illuminate\Cache\Repository::clear`](https://github.com/laravel/framework/blob/f43f238f35dc469bf577a493a9daa1518025739c/src/Illuminate/Cache/Repository.php#L580-L592) that returns bool and emits 3 types of events:
 - `\Illuminate\Cache\Events\CacheFlushing`
 - `\Illuminate\Cache\Events\CacheFlushed`
 - `\Illuminate\Cache\Events\CacheFlushFailed`


Please note to the `CacheFlushFailed`: it may happen for different reasons: locked file, unavailable Redis/memchace server, etc.

In this PR I would like to reflect Laravel's APi and event a new event for the failed attempt plus return boolean to indicate that the attempt is failed.

## Changes

This change addresses a real-world issue where cache clearing can fail, particularly when using the `file` cache driver due to file locking. In such scenarios:

- A file might be locked by `\Illuminate\Cache\FileStore::add` using a `LockableFile` with an exclusive lock
- The cache clearing operation would fail silently
- No event would be dispatched to notify the application of the failure

With these changes, applications can:

- Be notified of cache clearing failures via the `ClearingResponseCacheFailed` event
- Take appropriate action based on the boolean return value
- Implement retry mechanisms or fallback strategies when cache clearing fails

## Impact

This change improves error handling and observability without breaking existing functionality. Applications can now properly detect and respond to cache clearing failures, particularly in high-concurrency environments where file locking issues are more likely to occur.

## Backward Compatibility Breaks

For future release notes, the following backward compatibility breaks should be noted:

1. __Return Type Change__: The `ResponseCache::clear()` method now explicitly returns a boolean value instead of void. Code that type-hints this method's return as void will need to be updated.

2. __New Event__: The method now dispatches a new `ClearingResponseCacheFailed` event when cache clearing fails. Applications with event listeners that expect only the previous events (`ClearingResponseCache` and `ClearedResponseCache`) might need to handle this new event.

3. __Behavior Change__: Previously, the method would silently fail without providing any indication of failure. Applications that assumed the cache was always successfully cleared might need to be updated to check the return value and handle failure cases.


Diff:
diff --git a/README.md b/README.md
index 4e52d7e0..2a3fef51 100644
--- a/README.md
+++ b/README.md
@@ -455,11 +455,10 @@ This event is fired when a request passes through the `ResponseCache` middleware
 
 This event is fired when a request passes through the `ResponseCache` middleware but no cached response was found or returned.
 
-#### ClearingResponseCache and ClearedResponseCache
+#### ClearingResponseCache, ClearedResponseCache and ClearingResponseCacheFailed
 
-`Spatie\ResponseCache\Events\ClearingResponseCache`
-
-`Spatie\ResponseCache\Events\ClearedResponseCache`
+1. `Spatie\ResponseCache\Events\ClearingResponseCache`
+2. `Spatie\ResponseCache\Events\ClearedResponseCache` or `Spatie\ResponseCache\Events\ClearingResponseCacheFailed`
 
 These events are fired respectively when the `responsecache:clear` is started and finished.
 
diff --git a/src/Events/ClearingResponseCacheFailed.php b/src/Events/ClearingResponseCacheFailed.php
new file mode 100644
index 00000000..58508ab1
--- /dev/null
+++ b/src/Events/ClearingResponseCacheFailed.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Spatie\ResponseCache\Events;
+
+class ClearingResponseCacheFailed
+{
+}
diff --git a/src/ResponseCache.php b/src/ResponseCache.php
index 621fef6d..468ebfb1 100644
--- a/src/ResponseCache.php
+++ b/src/ResponseCache.php
@@ -8,6 +8,7 @@
 use Spatie\ResponseCache\CacheProfiles\CacheProfile;
 use Spatie\ResponseCache\Events\ClearedResponseCache;
 use Spatie\ResponseCache\Events\ClearingResponseCache;
+use Spatie\ResponseCache\Events\ClearingResponseCacheFailed;
 use Spatie\ResponseCache\Hasher\RequestHasher;
 use Symfony\Component\HttpFoundation\Response;
 
@@ -84,13 +85,19 @@ public function getCachedResponseFor(Request $request, array $tags = []): Respon
         return $this->taggedCache($tags)->get($this->hasher->getHashFor($request));
     }
 
-    public function clear(array $tags = []): void
+    public function clear(array $tags = []): bool
     {
         event(new ClearingResponseCache());
 
-        $this->taggedCache($tags)->clear();
+        $result = $this->taggedCache($tags)->clear();
 
-        event(new ClearedResponseCache());
+        $resultEvent = $result
+            ? new ClearedResponseCache()
+            : new ClearingResponseCacheFailed();
+
+        event($resultEvent);
+
+        return $result;
     }
 
     protected function addCachedHeader(Response $response): Response
diff --git a/src/ResponseCacheRepository.php b/src/ResponseCacheRepository.php
index f4a600bc..fcc35ef0 100644
--- a/src/ResponseCacheRepository.php
+++ b/src/ResponseCacheRepository.php
@@ -31,21 +31,22 @@ public function get(string $key): Response
         return $this->responseSerializer->unserialize($this->cache->get($key) ?? '');
     }
 
-    public function clear(): void
+    /**
+     * If the response cache tag is empty, or a Store doesn't support tags, the whole cache will be cleared.
+     
+     * @return bool Whether the cache was cleared successfully.
+     */
+    public function clear(): bool
     {
         if ($this->isTagged($this->cache)) {
-            $this->cache->flush();
-
-            return;
+            return $this->cache->flush();
         }
 
         if (empty(config('responsecache.cache_tag'))) {
-            $this->cache->clear();
-
-            return;
+            return $this->cache->clear();
         }
 
-        $this->cache->tags(config('responsecache.cache_tag'))->flush();
+        return $this->cache->tags(config('responsecache.cache_tag'))->flush();
     }
 
     public function forget(string $key): bool
diff --git a/tests/ResponseCacheClearTest.php b/tests/ResponseCacheClearTest.php
new file mode 100644
index 00000000..e6c20918
--- /dev/null
+++ b/tests/ResponseCacheClearTest.php
@@ -0,0 +1,42 @@
+<?php
+
+use Illuminate\Support\Facades\Event;
+use Mockery\MockInterface;
+use Spatie\ResponseCache\Events\ClearedResponseCache;
+use Spatie\ResponseCache\Events\ClearingResponseCache;
+use Spatie\ResponseCache\Events\ClearingResponseCacheFailed;
+use Spatie\ResponseCache\ResponseCache;
+use Spatie\ResponseCache\ResponseCacheRepository;
+
+beforeEach(function () {
+    Event::fake();
+});
+
+it('will fire appropriate events when clearing the cache successfully', function () {
+    $this->mock(ResponseCacheRepository::class, function (MockInterface $mock) {
+        $mock->shouldReceive('clear')
+            ->once()
+            ->andReturn(true);
+    });
+    $result = app(ResponseCache::class)->clear();
+
+    expect($result)->toBeTrue();
+    Event::assertDispatched(ClearingResponseCache::class);
+    Event::assertDispatched(ClearedResponseCache::class);
+    Event::assertNotDispatched(ClearingResponseCacheFailed::class);
+});
+
+it('will fire appropriate events when clearing the cache fails', function () {
+    $this->mock(ResponseCacheRepository::class, function (MockInterface $mock) {
+        $mock->shouldReceive('clear')
+            ->once()
+            ->andReturn(false);
+    });
+
+    $result = app(ResponseCache::class)->clear();
+
+    expect($result)->toBeFalse();
+    Event::assertDispatched(ClearingResponseCache::class);
+    Event::assertNotDispatched(ClearedResponseCache::class);
+    Event::assertDispatched(ClearingResponseCacheFailed::class);
+});


Reviews:
user: login: freekmurze
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-16T09:02:47Z
user: login: alies-dev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:04:21Z

Review comments:
diff_hunk: @@ -84,13 +85,19 @@ public function getCachedResponseFor(Request $request, array $tags = []): Respon
         return $this->taggedCache($tags)->get($this->hasher->getHashFor($request));
     }
 
-    public function clear(array $tags = []): void
+    public function clear(array $tags = []): bool
     {
         event(new ClearingResponseCache());
 
-        $this->taggedCache($tags)->clear();
+        $result = $this->taggedCache($tags)->clear();
 
-        event(new ClearedResponseCache());
+        if ($result === true) {
path: src/ResponseCache.php
user: login: freekmurze
body: Avoid using `else` by refactoring to a ternary to determine the event classname.
created_at: 2025-05-16T09:02:36Z
author_association: MEMBER
diff_hunk: @@ -84,13 +85,19 @@ public function getCachedResponseFor(Request $request, array $tags = []): Respon
         return $this->taggedCache($tags)->get($this->hasher->getHashFor($request));
     }
 
-    public function clear(array $tags = []): void
+    public function clear(array $tags = []): bool
     {
         event(new ClearingResponseCache());
 
-        $this->taggedCache($tags)->clear();
+        $result = $this->taggedCache($tags)->clear();
 
-        event(new ClearedResponseCache());
+        if ($result === true) {
path: src/ResponseCache.php
user: login: alies-dev
body: done by https://github.com/spatie/laravel-responsecache/pull/495/commits/f63a605018593ad5d42bac407c31d0887aa1e8bf
created_at: 2025-05-16T11:04:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: alies-dev
created_at: 2025-05-19T21:18:52Z
body: hey @freekmurze 

is anything I can do to speed up the process of this PR review? Do you have any concerns about this idea or the implementation?
author_association: CONTRIBUTOR
user: login: freekmurze
created_at: 2025-05-20T08:39:08Z
body: Thanks!
author_association: MEMBER