Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Psr7 Cache Laravel Package

micheh/psr7-cache

PSR-7 HTTP caching middleware for PHP that stores and reuses responses according to RFC 7234, handles cache headers and conditional requests, and integrates with PSR-7/PSR-15 stacks to reduce backend load.

View on GitHub
Deep Wiki
Context7

Getting Started

This release (0.5) is additive — no breaking changes — so upgrading is a composer update away. Pull it in and re-run your test suite; the only behavioural difference existing code can notice is that If-None-Match now also matches weak ETags (W/"abc"), which is a correctness fix rather than a regression.

Where to look first:

  1. The util object — the package exposes a single service (referred to as $util in the release notes) that you inject into a controller, resolve from the container, or wrap in your own ConditionalRequests helper. Everything in this package flows through that one class, so read its public methods end to end (there are only a handful: state validator detection, current-state checks, and the comparison internals).
  2. The two new methods — hasStateValidator($request) and hasCurrentState($request, $eTag, $lastModified). The first tells you whether the incoming request carries If-Match / If-Unmodified-Since (i.e. the client is making an unsafe conditional request); the second is the actual precondition evaluation.
  3. Your existing read endpoints — the fastest first win is still the safe path: attach an ETag to a GET response and short-circuit to 304 when hasCurrentState() is true.

First use case — put it on a "show" endpoint:

public function show(Request $request, Post $post, ConditionalRequest $util)
{
    $eTag = hash('xxh128', $post->updated_at.$post->id);

    if ($util->hasCurrentState($request, $eTag, $post->updated_at)) {
        return response()->noContent(304)->setEtag($eTag);
    }

    return response()
        ->json($post)
        ->setEtag($eTag)
        ->setLastModified($post->updated_at);
}

Then add the unsafe half — the reason 0.5 exists — to your write endpoints:

public function update(Request $request, Post $post, ConditionalRequest $util)
{
    if ($util->hasStateValidator($request) && ! $util->hasCurrentState($request, $eTag, $post->updated_at)) {
        abort(412, 'Precondition Failed'); // client is editing a stale copy
    }

    // ... apply the update
}

Implementation Patterns

Centralise the validators, don't scatter them. Compute the ETag in one place — an accessor, a HasETag trait, or a small ResourceValidator class — and reuse it for both the response header and the precondition check. Mismatched ETag generation between read and write paths is the number one source of "412 on everything" bugs.

Middleware over per-action boilerplate. For read-heavy APIs, a RevalidateCache middleware that computes the ETag from the response and converts to 304 is cleaner than repeating the check in every controller:

return $next($request)->tap(function ($response) use ($request, $util) {
    if ($response->isSuccessful() && $request->isMethodSafe() && $util->hasCurrentState($request, $response->getEtag(), $response->getLastModified())) {
        // strip body, set 304
    }
});

Register it globally for GET/HEAD only — never for unsafe methods.

Optimistic concurrency for writes. The idiomatic flow for PUT/PATCH/DELETE:

Situation Response
No If-Match/If-Unmodified-Since sent Your choice: process normally, or 428 Precondition Required if you want to force clients to be careful
Validator sent and matches current state Process the write, return the new ETag
Validator sent and does not match 412 Precondition Failed with the current representation so the client can merge

Always return the fresh ETag after a successful write so the client can chain requests without a re-read.

Combine with a row lock, not instead of one. If-Match is optimistic concurrency at the HTTP layer; it does not stop two requests with the same valid ETag racing. Wrap the check and the write in DB::transaction() with lockForUpdate() (or rely on a version column updated with a where version = ? clause) if the resource is contended.

Prefer ETags for writes, Last-Modified as a fallback. HTTP dates have one-second resolution, so two updates within the same second are indistinguishable to If-Unmodified-Since. Emit Last-Modified for compatibility and cache friendliness, but make the ETag the authoritative validator (derive it from updated_at + a version/counter column, not just updated_at).

Use weak semantics where equivalence ≠ byte-identity. With 0.5's weak comparison, W/"abc" correctly satisfies If-None-Match: "abc" and vice versa. That's what you want for GET where the representation is semantically identical (e.g. JSON key ordering, whitespace, gzip differences). Keep strong ETags on anything you'll later guard with If-Match.

Testing. Laravel's test helpers make this trivial — assert the headers on the first response and replay them:

$first = $this->getJson("/api/posts/{$post->id}")->assertOk();
$this->withHeaders(['If-None-Match' => $first->headers->get('ETag')])
     ->getJson("/api/posts/{$post->id}")
     ->assertStatus(304);

$this->withHeaders(['If-Match' => '"bogus"'])
     ->patchJson("/api/posts/{$post->id}", ['title' => 'x'])
     ->assertStatus(412);

Add a weak-variant test (W/ + the same opaque tag) — that path is new in 0.5 and worth pinning down so a future release doesn't silently change it.

Document the contract. Clients need to know they should store the ETag from a GET and send it as If-Match on the subsequent write. Add it to your OpenAPI spec (ETag response header, If-Match/If-None-Match parameters, 304/412/428 responses) so generated clients handle it.

Gotchas and Tips

  • Don't 304 an unsafe method. A failed If-None-Match on PUT/PATCH/DELETE is 412 Precondition Failed, not 304. Only GET/HEAD get the not-modified shortcut. hasCurrentState() intentionally does the comparison only — the status code choice is yours, and it's easy to get wrong in a shared middleware.
  • If-Match must use strong comparison (RFC 9110). A weak W/"..." validator must never satisfy If-Match. 0.5 added weak comparison for If-None-Match specifically; verify with a test that your If-Match path still rejects weak tags, and avoid emitting W/ ETags on resources you intend to guard with If-Match.
  • Handle If-Match: *. Per spec, * means "the resource must exist" and matches any current representation. If the util doesn't special-case it, you'll return spurious 412s to well-behaved clients. Test it.
  • Second-resolution timestamps. If-Unmodified-Since cannot distinguish two writes in the same second — classic lost-update bug on fast endpoints. Always pair with an ETag or a monotonically incrementing version column.
  • Order your middleware deliberately. Put auth/authorize before the precondition check, otherwise a 412 vs 403 difference leaks whether a resource exists (and lets an unauthorised caller learn its ETag). Put the precondition check before any write-side side effects (queued jobs, events) so a failed precondition costs nothing.
  • Last-Modified must be GMT. Laravel will format a Carbon instance, but if you hand it a string or a local-time DateTime you can end up with a header the client parses wrong — always ->setLastModified($model->updated_at->utc()) or an immutable_datetime cast.
  • ETag quoting. ETags must be wrapped in double quotes ("abc", W/"abc") — Response::setEtag($value) handles quoting, but if you build the header manually or compare raw header strings you'll get mismatches. Use the package's comparison rather than === on header values.
  • Set Cache-Control alongside your validators. An ETag with Cache-Control: public, max-age=3600 means clients won't revalidate at all. Use no-cache (store but always revalidate) or private, must-revalidate for per-user data. Add Vary on Authorization/Accept-Encoding/Accept where relevant, or a CDN will happily serve one user's 304 decision to another.
  • Beware noContent(304) in tests. A 304 has no body — assertJson/assertSee will fail and assertNoContent() may be too loose. Assert the status and headers explicitly; also assert that your normal 200 body is unaffected.
  • Extend via the util, not by forking. If you need custom comparison rules (e.g. an If-Range implementation, or If-None-Match: * on POST), subclass or decorate the util and bind it in a service provider — that keeps you on the upgrade path for the next release's comparison fixes, which is exactly what 0.5 was.
  • Upgrade checklist for 0.5. (1) Search for any hand-rolled weak/strong ETag comparison in your app and replace it with the util. (2) Add tests for weak If-None-Match and for If-Match/If-Unmodified-Since. (3) Confirm your write endpoints now return 412 rather than silently applying stale updates — some clients may surface this as a new error, so coordinate before deploying.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle