@@ -60,8 +60,12 @@ public function apply($buffer)
6060 public function handle ($ request , Closure $ next )
6161 {
6262 // Only cache GET requests
63+ // Mutating requests should bust relevant cache entries
6364 if (! $ request ->isMethod ('GET ' )) {
64- return $ next ($ request );
65+ $ response = $ next ($ request );
66+ $ this ->invalidateCacheIfNeeded ($ request );
67+
68+ return $ response ;
6569 }
6670
6771 // Check if caching is enabled
@@ -95,6 +99,26 @@ public function handle($request, Closure $next)
9599 return $ response ;
96100 }
97101
102+ /**
103+ * Invalidate cache entries for mutating requests when configured.
104+ *
105+ * @param \Illuminate\Http\Request $request
106+ * @return void
107+ */
108+ protected function invalidateCacheIfNeeded ($ request )
109+ {
110+ if (! config ('laravel-page-speed.api.cache.enabled ' , false )) {
111+ return ;
112+ }
113+
114+ $ methods = config ('laravel-page-speed.api.cache.purge_methods ' , ['POST ' , 'PUT ' , 'PATCH ' , 'DELETE ' ]);
115+ if (! in_array (strtoupper ($ request ->getMethod ()), $ methods , true )) {
116+ return ;
117+ }
118+
119+ $ this ->invalidateCache ($ request );
120+ }
121+
98122 /**
99123 * Generate a unique cache key for the request.
100124 *
@@ -159,6 +183,7 @@ protected function putInCache($cacheKey, $response, $request)
159183 try {
160184 $ ttl = $ this ->getCacheTTL ($ request );
161185 $ driver = config ('laravel-page-speed.api.cache.driver ' , 'redis ' );
186+ $ store = Cache::store ($ driver );
162187
163188 $ cacheData = [
164189 'content ' => $ response ->getContent (),
@@ -170,11 +195,8 @@ protected function putInCache($cacheKey, $response, $request)
170195 // Use cache tags if supported (Redis, Memcached)
171196 $ tags = $ this ->getCacheTags ($ request );
172197
173- if (! empty ($ tags ) && in_array ($ driver , ['redis ' , 'memcached ' ])) {
174- Cache::store ($ driver )->tags ($ tags )->put ($ cacheKey , $ cacheData , $ ttl );
175- } else {
176- Cache::store ($ driver )->put ($ cacheKey , $ cacheData , $ ttl );
177- }
198+ $ store ->put ($ cacheKey , $ cacheData , $ ttl );
199+ $ this ->indexCacheKey ($ store , $ cacheKey , $ tags , $ ttl );
178200
179201 Log::debug ('API response cached ' , [
180202 'key ' => $ cacheKey ,
@@ -189,6 +211,124 @@ protected function putInCache($cacheKey, $response, $request)
189211 }
190212 }
191213
214+ /**
215+ * Invalidate cached entries related to a mutation request.
216+ *
217+ * @param \Illuminate\Http\Request $request
218+ * @return void
219+ */
220+ protected function invalidateCache ($ request )
221+ {
222+ $ invalidated = false ;
223+
224+ try {
225+ $ driver = config ('laravel-page-speed.api.cache.driver ' , 'redis ' );
226+ $ store = Cache::store ($ driver );
227+ $ tags = $ this ->getCacheTags ($ request );
228+
229+ if (! empty ($ tags )) {
230+ $ invalidated = $ this ->flushIndexedKeys ($ store , $ tags );
231+ }
232+
233+ if (! $ invalidated ) {
234+ $ cacheKey = $ this ->generateCacheKey ($ request );
235+ $ invalidated = $ store ->forget ($ cacheKey );
236+ }
237+ } catch (\Exception $ e ) {
238+ Log::warning ('API cache invalidation failed ' , [
239+ 'method ' => $ request ->getMethod (),
240+ 'uri ' => $ request ->getRequestUri (),
241+ 'error ' => $ e ->getMessage (),
242+ ]);
243+ }
244+
245+ return $ invalidated ;
246+ }
247+
248+ /**
249+ * Determine if cache store supports tags.
250+ *
251+ * @param \Illuminate\Cache\Repository $store
252+ * @return bool
253+ */
254+ /**
255+ * Keep index of cache keys per tag group for manual invalidation.
256+ *
257+ * @param \Illuminate\Cache\Repository $store
258+ * @param string $cacheKey
259+ * @param array $tags
260+ * @param int $ttl
261+ * @return void
262+ */
263+ protected function indexCacheKey ($ store , $ cacheKey , array $ tags , $ ttl )
264+ {
265+ if (empty ($ tags )) {
266+ return ;
267+ }
268+
269+ try {
270+ $ indexKey = $ this ->getTagIndexKey ($ tags );
271+ $ keys = $ store ->get ($ indexKey , []);
272+ $ keys [$ cacheKey ] = now ()->addSeconds ($ ttl )->getTimestamp ();
273+
274+ // Keep index fresh slightly longer than cache TTL to ensure cleanup
275+ $ store ->put ($ indexKey , $ keys , max ($ ttl , 600 ));
276+ } catch (\Exception $ e ) {
277+ Log::debug ('API cache index write failed ' , [
278+ 'key ' => $ cacheKey ,
279+ 'tags ' => $ tags ,
280+ 'error ' => $ e ->getMessage (),
281+ ]);
282+ }
283+ }
284+
285+ /**
286+ * Flush cached responses tracked under tags index.
287+ *
288+ * @param \Illuminate\Cache\Repository $store
289+ * @param array $tags
290+ * @return bool
291+ */
292+ protected function flushIndexedKeys ($ store , array $ tags )
293+ {
294+ try {
295+ $ indexKey = $ this ->getTagIndexKey ($ tags );
296+ $ keys = $ store ->get ($ indexKey , []);
297+
298+ if (empty ($ keys )) {
299+ return false ;
300+ }
301+
302+ foreach (array_keys ($ keys ) as $ cacheKey ) {
303+ $ store ->forget ($ cacheKey );
304+ }
305+
306+ $ store ->forget ($ indexKey );
307+
308+ return true ;
309+ } catch (\Exception $ e ) {
310+ Log::debug ('API cache index flush failed ' , [
311+ 'tags ' => $ tags ,
312+ 'error ' => $ e ->getMessage (),
313+ ]);
314+ }
315+
316+ return false ;
317+ }
318+
319+ /**
320+ * Build deterministic index key for a given tag list.
321+ *
322+ * @param array $tags
323+ * @return string
324+ */
325+ protected function getTagIndexKey (array $ tags )
326+ {
327+ sort ($ tags );
328+
329+ return self ::CACHE_PREFIX . 'tag_index: ' . md5 (implode ('| ' , $ tags ));
330+ }
331+
192332 /**
193333 * Create response from cached data.
194334 *
0 commit comments