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

Laravel Last Modified Laravel Package

abordage/laravel-last-modified

Laravel Last Modified adds automatic last-modified tracking to your Eloquent models, keeping timestamps in sync on create, update, and delete. Use it to power HTTP caching headers, sitemap dates, or "updated" badges with minimal setup and no extra tables.

View on GitHub
Deep Wiki
Context7

Getting Started

abordage/laravel-last-modified is a small HTTP-caching utility: it attaches a Last-Modified header to a response and short-circuits with 304 Not Modified when the client sends a matching If-Modified-Since. It is not a full response cache — it saves bandwidth and render time, not database queries you've already run before the middleware fires.

Minimal setup:

  1. composer require abordage/laravel-last-modified
  2. Publish the config if you want to tune it: php artisan vendor:publish --tag=last-modified-config (check the README / src/ for the exact tag and class names in your installed version — this package is small and the surface area is deliberately narrow).
  3. Register the package middleware on the routes that should be cache-validated, either in bootstrap/app.php (->withMiddleware(...)) on Laravel 12/13, or per-route-group.

First use case: a mostly-static marketing or docs page, or a read-only API endpoint backed by a table with updated_at. Point the middleware at the route, hit it twice with a browser or curl -H "If-Modified-Since: <value>", and confirm the second response is a bodiless 304.

Version note for 1.1.0: the release is compatibility and CI housekeeping only — Laravel 13 is now supported and tested, Laravel 11 is no longer in the test matrix. No new features, no deprecations, no config changes. If you are on Laravel 11, check composer.json's illuminate/* constraint before assuming you're cut off; "stopped testing" is not the same as "stopped allowing," but you are now on an untested path.

Where to look first: the published config file (it tells you what sources the package can derive a timestamp from), then the middleware class itself — it's short enough to read end-to-end and that is the fastest way to understand precedence rules.

Implementation Patterns

Route-group scoping. Apply the middleware to groups, not globally. A global registration means every authenticated, personalised, session-flavoured response is a candidate for a 304, which is almost never what you want.

Route::middleware('last-modified')->group(function () {
    Route::get('/docs/{page}', [DocsController::class, 'show']);
});

Timestamp from the data, not the clock. The interesting decision is what "last modified" means for a given route. Sensible sources, roughly in order of correctness:

  • max(updated_at) (or max(deleted_at) too, if deletions should invalidate) on the underlying table
  • The mtime of a generated artifact, sitemap, or compiled asset
  • Git commit time of the deployed release — good for "content shipped with the code" pages, but note it changes on every deploy even when the page didn't

Avoid now() or filemtime() on a file that your deploy pipeline rewrites; both cause permanent cache misses.

Pair with Cache-Control. Last-Modified on its own gives clients nothing to work with. Set an explicit Cache-Control: public, max-age=… (or no-cache if you want revalidation every time but still want the cheap 304 path). This package handles the validation half; you own the freshness half.

Model-backed endpoints. A tidy pattern for JSON APIs:

$lastModified = Article::max('updated_at');

return response()
    ->json($articles)
    ->setLastModified($lastModified ? Carbon::parse($lastModified) : null);

If the package exposes a fluent/response-macro helper, prefer that over hand-setting the header — it keeps the comparison logic in one place. If it doesn't, setLastModified() on the response plus your own If-Modified-Since check is only a few lines.

Testing. $this->get(...)->assertStatus(304) and assertHeader('Last-Modified', ...). Send the header explicitly: $this->withHeader('If-Modified-Since', $lastModified->toRfc7231String()). Freeze time ($this->travelTo(...)) so the one-second granularity of HTTP dates doesn't make the test flaky.

Integration with ETags. If you already emit ETags, Last-Modified is a weaker validator and the two coexist: clients sending both should get 304 only if both match (or per RFC 9110, the ETag takes precedence). Don't hand-roll that; if you need both, do the ETag check first and let this package handle the date path.

Gotchas and Tips

  • 304 responses have no body. Any test asserting assertSee on a revalidated request will fail. Assert status and headers instead. Same for curl — add -i if you're confused about why "the page is empty."
  • One-second granularity. HTTP dates have no sub-second precision, and comparisons are "not modified since," so an update in the same second as the previous request can be missed. For high-churn data this is a real correctness wart; an ETag over the content hash is the honest fix.
  • Clock skew and future dates. If a DB timestamp or server clock lands in the future relative to the client, you can pin a client into permanent revalidation. Sanitise with ->min(now()) when the source is untrusted.
  • Deployment churn. If your timestamp source is a file mtime, every deploy invalidates everything — which is fine — but if it's storage/ files mutated at runtime, you'll get the opposite problem. Log your computed timestamp for a few routes in staging and sanity-check it against reality.
  • Personalised content is the classic footgun. Anything behind auth, flash messages, CSRF-tokened forms, or A/B-test middleware must not be served from a shared validator. If the response varies per user, Last-Modified must reflect that variance or not be set at all.
  • Config caching. After php artisan config:cache, changes to config/last-modified.php are invisible until you re-cache. Standard Laravel trap, but it bites harder on tiny packages where you're editing config during debugging.
  • Middleware order. The validator should run after anything that could change the body and before expensive rendering, which is usually impossible to arrange cleanly — accept that the first request pays full cost and only the revalidation is cheap.
  • artisan route:list -v is the fastest way to confirm the middleware actually landed on the route you think it did. Half the "it's not working" reports are a group that never got the middleware.
  • Laravel 11 is untested from 1.1.0 onward. If you're pinned to 11, either stay on 1.0.x, or verify the composer constraint plus a smoke test on your own app before upgrading. CI matrices get trimmed for maintenance-burden reasons, not because the code broke — but "untested" is a risk you're accepting, not an absence of risk.
  • Extension points are thin by design. If you need per-route timestamp resolvers or a custom comparison, expect to write a small subclass or wrapper rather than find a hook — this package is a few hundred lines and reading it is cheaper than searching for configuration that doesn't exist.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core