Repository navigation
Tag versioning: replace Laravel cache tags (v2) - #30
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q
ResponseCache and the middleware still call the removed Repository API, so the CacheTest, TagBindingTest and ResponseCacheTest suites fail until the following commits rewire them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q
ResponseCache and the middleware still call the removed Repository API until the next commits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q
The CacheResponse middleware still calls the removed has() until the next commit, so feature tests and PHPStan fail on it until then. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q
…ocument Miss change Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q
- Validate the store and tag_ttl in the Repository constructor, so the checks hold however it is built - Write all versions of a flush in one setMultiple() call - De-duplicate tags only in ResponseCache::resolveTags() - BaseStrategy::shouldCache() defers to isCacheable() - Merge near-identical command tests into one with a dataset Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q
- getDefaultTtl() casts RESPONSE_CACHE_TTL, which arrives as a string - Entries expire after tag_ttl at the latest, since their versions do; entries without a TTL no longer stay in the store forever - Document serializable_classes and null snapshots Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q
…enabled - Flushes wait until all open transactions on every connection have ended, and run after rollbacks too: the data may have been written outside the transaction or on another connection, so dropping them could serve stale responses - A deferred flush that fails is reported instead of thrown, so it no longer aborts DB::commit() and the application's other after-commit callbacks - ResponseCache::get() no longer creates tag versions - enabled() accepts boolean strings from the environment Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q
- Ignore transactions Laravel still lists as pending although their connection has left them: during a full rollback, and after a failed commit. Flushes registered from rollback callbacks were lost, and every flush after a failed commit stayed deferred until the next commit - Skip transactions opened by testing traits such as RefreshDatabase, which deferred all flushes in application tests until the test ended - enabled() keeps the default for unset or unrecognized values Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q
Found by running a fresh Laravel 13 app against Redis, Memcached, Postgres
and the file store:
- New Laravel 13 apps set cache.serializable_classes to false, so cached
Response objects came back as __PHP_Incomplete_Class and every lookup on
Redis, database and file stores missed. Entries now hold the status,
headers and content, and hits are restored as Illuminate\Http\Response.
Responses without content (streamed, file downloads) are not cached.
- A tag binding to a model with an integer route key, like user:{user},
passed an int to str_replace() under strict_types and failed the request.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q
2 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces Laravel cache tags with tag versioning. Laravel tags are part of the cache key (an entry stored with
[a, b]can only be read with exactly[a, b]), which made several operations silently do nothing or clear far too much.How it works now
failoverstore is refused.Bugs fixed on
masterresponse-cache:clear/ResponseCache::flush()without tags did nothing on taggable stores. With default tags configured,delete($uri)and the command also did nothing.file).flush(['x'])merged in the default tags, so flushing one tag cleared everything.RESPONSE_CACHE_TTLandRESPONSE_CACHE_ENABLEDfrom.envthrew aTypeErrorunderstrict_types.user:{user}) threw aTypeErrorunderstrict_types.cache.serializable_classestofalse, cached responses could not be read back from Redis, database or file stores, so nothing was ever served from the cache. Entries are now stored as plain data (status, headers, content).Breaking changes (v2)
The README has a full Upgrading to v2 section. The main changes:
CacheStrategy::tags(Request $request): the response parameter is gone. There is a newCacheStrategy::isCacheable(Request $request).flush()flushes everything,flush([])flushes nothing, and default tags are no longer merged in.ResponseCache::has()is removed.put()now takes aSnapshot(fromlookup()orsnapshot()) instead of tags.storedefaults to the app's default cache store, and there is a newtag_ttloption (30 days).isCacheable(e.g.POST) no longer emitMiss.Repositorysubclass is removed.The README also has a new Limitations section (data read before the snapshot, read replicas, store failover, DynamoDB, local stores, hit rate).
Test plan
composer test: 73 passing. New unit tests forRepositoryand new feature tests inInvalidationTestcover:application tests wrapped in
RefreshDatabase/DatabaseTransactionstag_ttlget()not writing to the storecomposer analyze(PHPStan) is clean.vendor/bin/pint --testis clean.response-cache:clearwith and without tags (from the CLI),delete(), and foreign keys surviving a flush-allPOSTrequests, and 30 concurrent cold requeststag_ttlexpiry🤖 Generated with Claude Code
https://claude.ai/code/session_01GGNGyJLoFTM5C474gZPW5q