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

Cache Control Bundle Laravel Package

kitpages/cache-control-bundle

Symfony bundle for managing HTTP Cache-Control headers via annotations and configuration, letting you define per-action caching policies and set public/private, max-age, s-maxage, and no-cache directives.

View on GitHub
Deep Wiki
Context7

Getting Started

First, calibrate expectations: this package will not run in Laravel. kitpages/cache-control-bundle is a Symfony2 bundle — it depends on Symfony\Component\HttpKernel\Bundle\Bundle, the DependencyInjection\Extension contract, and kernel.response listeners registered through Symfony's event dispatcher. composer require kitpages/cache-control-bundle will succeed (MIT, PHP-only) and then do nothing: there is no Laravel service provider, no auto-discovered middleware, no registerBundles(). Treat the repo as a specification, not a dependency.

Your first move is therefore not installation but reading the source. You're looking for two files:

  1. The response listener (typically EventListener/...ResponseListener.php or similar). This is where the literal header map lives. Extract it verbatim:
    • Cache-Control: no-cache, no-store, must-revalidate (confirm the exact string — this is the one thing worth copying precisely)
    • Pragma: no-cache
    • Expires: 0 (verify — may be a formatted date)
  2. The DI extension / Configuration.php. Skim only to see whether the bundle supports per-route configuration (public/private, max-age) or is a blunt global toggle. In practice, the "no cache on every page" use case is global, and the config surface is irrelevant to you.

Ten minutes of reading replaces the dependency entirely.

Your first use case is almost always the deploy-staleness ticket: "users see the old version after we ship." Before writing code, confirm that's the actual problem — because if stale JS/CSS is the symptom, the fix is Vite's hashed filenames, not HTTP headers. Reserve the no-cache middleware for HTML and dynamic JSON responses.

Minimal steps to reproduce the behavior first-party:

  1. Create app/Http/Middleware/PreventBrowserCache.php:
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class PreventBrowserCache
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        // Mirror the header set from the bundle's response listener.
        $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0');
        $response->headers->set('Pragma', 'no-cache');
        $response->headers->set('Expires', '0');

        return $response;
    }
}
  1. Register it as an alias, not as global middleware (see Gotchas — ordering matters):
// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'no-cache' => \App\Http\Middleware\PreventBrowserCache::class,
    ]);
})
  1. Apply it where you actually need it:
Route::middleware('no-cache')->group(function () {
    Route::get('/dashboard', DashboardController::class);
    Route::get('/account/{id}', AccountController::class);
});
  1. Verify against a real environment, not just artisan serve:
curl -sI https://staging.example.test/dashboard \
  | grep -iE '^(cache-control|pragma|expires|age|x-cache):'

You are looking for your three headers and the absence of a caching layer overwriting them (Age: and X-Cache: reveal whether a CDN/proxy is still serving a cached copy).

Implementation Patterns

Route-group scoping (preferred). This is the pattern the bundle should have supported and mostly didn't. Apply no-cache to the dynamic, authenticated surfaces; leave marketing pages, API GETs, and asset responses cacheable.

// Authenticated / personalized — never cache
Route::middleware(['web', 'auth', 'no-cache'])->group(function () {
    Route::get('/dashboard', ...);
});

// Public marketing — cache aggressively instead
Route::middleware(['web', 'cache:public, max-age=600'])->group(function () {
    Route::get('/', ...);
    Route::get('/pricing', ...);
});

A small config file for the policy. Keep the header strings in one place rather than scattered across middleware, so the next reader doesn't re-litigate no-cache vs no-store:

// config/cache-control.php
return [
    'no_cache' => [
        'Cache-Control' => 'no-cache, no-store, must-revalidate, max-age=0',
        'Pragma'        => 'no-cache',
        'Expires'       => '0',
    ],
    'no_cache_routes' => [
        'dashboard', 'account.*', 'api/internal.*',
    ],
];

Respect an existing header (opt-out pattern). Let controllers override the middleware when they know better — e.g. an endpoint that legitimately wants public, max-age=60:

$response = $next($request);

if (! $response->headers->has('Cache-Control')) {
    foreach (config('cache-control.no_cache') as $name => $value) {
        $response->headers->set($name, $value);
    }
}

return $response;

This mirrors how the Symfony bundle's listener coexists with controller-set headers, and it keeps the middleware from silently clobbering ETag/Vary work done downstream.

Conditional on auth, not just route. For a mixed route that renders differently when logged in, branch on $request->user():

if ($request->user()) {
    $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
    $response->headers->set('Vary', 'Cookie');
}

Testing pattern. This is where the day-to-day leverage lives — assert the policy so a future middleware change doesn't silently regress it:

it('sends no-cache headers on the dashboard', function () {
    $this->actingAs(User::factory()->create())
        ->get('/dashboard')
        ->assertHeader('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0')
        ->assertHeader('Pragma', 'no-cache');
});

assertHeader is an exact string match — keep the config value and the assertion in sync, or assert with assertHeader('Cache-Control') (presence-only) plus a str_contains if you expect variation.

Deploy-time complement. The middleware is the secondary measure. The primary fix for "stale after deploy" is Vite manifest hashing plus a CDN purge step in the pipeline. Document both in the same PR so a future reader doesn't conclude the middleware alone was the solution.

If you genuinely are on Symfony (legacy 2.x maintenance), the bundle's own pattern is: enable it in AppKernel::registerBundles(), import its config in config.yml, and it registers a kernel.response listener that stamps every response. That is the only context in which composer require does anything — and even there, a kernel response listener in src/ is a better long-term owner.

Gotchas and Tips

The silent no-op is the worst failure mode. If someone adds this to composer.json believing it enforces no-cache, nothing happens — no error, no log line, no headers. Stale content incidents continue and the team "already fixed it." If you must keep the package in composer.json for reference, leave a comment and a failing test proving it's inert; better, delete it.

Middleware ordering will bite you. Laravel's global middleware wraps the router, so its response phase runs last — after route middleware and after the controller. A global no-cache middleware will overwrite headers set by a cache:public route middleware. Keep no-cache as an alias applied to route groups so the ordering is explicit. If you do need it global, make it honor an existing Cache-Control header as shown above.

Local vs production header divergence. artisan serve has no nginx in front. Your headers will look perfect locally and be overwritten in production by nginx expires/add_header, proxy_cache, or the CDN's own edge policy. Always verify with curl -I against staging/prod and inspect Age:, X-Cache:, CF-Cache-Status: to identify which layer is answering.

no-cache ≠ no-store, and no-store has UX cost. no-cache means "revalidate before use"; no-store means "don't write it to disk at all." no-store disables the browser back-forward cache in several browsers, making back-navigation slow. If the requirement is only freshness, no-cache, must-revalidate is often sufficient and cheaper.

Pragma is HTTP/1.0 legacy and request-only per spec. Modern clients ignore it; it exists to appease ancient proxies. Keeping it is harmless but don't debug against it.

Expires: 0 is an invalid HTTP date that works because everyone special-cases it. If you need stricter spec compliance, use Expires: Thu, 01 Jan 1970 00:00:00 GMT. Pick one and stay consistent — the bundle's choice is a convention, not a rule.

Don't copy the source into your repo. The listener uses FilterResponseEvent/GetResponseEvent, which were renamed to ResponseEvent/RequestEvent in Symfony 4.3 and removed earlier. Laravel 10/11 ship Symfony 6/7 HTTP Foundation. A verbatim port won't typecheck. Only the HTTP-spec-stable header strings port cleanly.

Blanket no-store is a performance tax, not a free win. It defeats CDN and browser caching for every response it touches, multiplying origin traffic and regressing TTFB — potentially by an order of magnitude on anything asset-like. Scope the middleware to dynamic HTML/JSON, and confirm the intended CDN behavior with whoever owns the edge config before enabling it site-wide. This is a bigger risk than the dependency choice itself.

Watch for under-application too. Headers set only on the web group leave API/JSON responses cacheable by clients. Apply the alias to both web and api stacks, or use a shared group.

Debugging technique: send Cache-Control: no-cache on the request to bypass intermediate caches while testing, and check whether a Vary header is present on any response whose content depends on session/cookie — caching a personalized page without Vary: Cookie is a data-leak waiting to happen.

Extension points: the bundle's config schema (public/private, max-age) is richer than the "no cache everywhere" use case needs. If you want max-age control in Laravel, model it as a separate cache: middleware reading the same config/cache-control.php — there is nothing in the bundle worth porting beyond the header strings.

Octane-safe: header mutation is per-response, no shared state — no concerns if you run Laravel Octane.

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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast