mapado/cache-info-bundle
A Symfony bundle that surfaces cache information in your app, adding a cache panel to the web debug toolbar and profiler so you can inspect cache pools, hits, misses, and stored items while debugging.
Accept that this is a reading exercise, not an install. mapado/cache-info-bundle is a Symfony FrameworkBundle extension registered through AppKernel/bundles.php and wired via a DependencyInjection compiler pass. It declares no extra.laravel.providers block, so Composer autodiscovery will register nothing in a Laravel app, and its symfony/http-kernel pins will collide with the Symfony components Laravel already constrains. It is also archived with one star and zero dependents — no maintainer, no security patches. Treat it as a reference implementation to skim and port, not a dependency.
Where to look first (clone locally, ~15 minutes):
composer.json — confirms the Symfony generation it targets. Older Symfony 3/4 pins are a hard "do not install" signal for a modern Laravel/Symfony 6/7 stack.DependencyInjection/*Extension.php + Resources/config/services.* — reveals which services exist and what arguments they receive. That argument list is effectively the config schema: which response headers it inspects, whether it writes headers back, whether it hooks the profiler.EventSubscriber/EventListener directory — the core. Expect a kernel.response listener that reads HTTP cache headers (X-Symfony-Cache, X-Cache, CF-Cache-Status, Age, Cache-Control, Expires, ETag) off the response and stashes them somewhere readable.Resources/views/ — if there is a WebProfiler template, this is the "presentation layer" telling you which fields the authors considered valuable enough to surface. That list is your spec.First use case to build in Laravel. You want to know, in local and staging, whether a response was served by your CDN/reverse proxy or hit PHP, and why an endpoint is or isn't cacheable. That is the actual job the Symfony bundle does — turning opaque headers into a readable signal. Reimplement that minimally:
// app/Http/Middleware/CacheInfo.php
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
try {
if (! config('cache-info.enabled')) {
return $response;
}
$map = config('cache-info.headers');
$info = collect($map)
->mapWithKeys(fn ($label, $header) => [$label => $response->headers->get($header)])
->filter()
->all();
Context::add('cache_info', $info);
} catch (\Throwable $e) {
report($e); // fail open — diagnostics must never 500 a request
}
return $response;
}
Register it globally and last so it observes the final, fully constructed response.
Pattern 1 — Header-mapping config, not hard-coded reads. The Symony bundle hard-codes a small set of cache headers in its listener. Do yourself the favor of externalizing that mapping so different proxies (Fastly, Cloudflare, Varnish, Symfony HttpCache, Nginx X-Cache-Status) can be described without touching code:
// config/cache-info.php
return [
'enabled' => env('CACHE_INFO_ENABLED', false),
'headers' => [
'X-Cache' => 'proxy_status', // HIT / MISS / STALE
'CF-Cache-Status' => 'cloudflare',
'Age' => 'age_seconds',
'Cache-Control' => 'cache_control',
'X-Symfony-Cache' => 'symfony_http_cache',
],
'emit_header' => env('CACHE_INFO_HEADER', false), // only in local/staging
];
This mirrors the bundle's argument-injection pattern (services configured via services.yml) without carrying Symfony's DI machinery into Laravel.
Pattern 2 — Context for structured output, headers only when gated. The Symfony equivalent of "surfacing to developers" is the profiler panel. In Laravel, the equivalent surfaces are Telescope, logs, and — optionally — a response header the browser devtools can reveal. Use Illuminate\Support\Facades\Context so the info is attached to the corresponding log lines and Telescope entries automatically; only emit an actual X-Cache-Info header when explicitly enabled and the request is not going through a shared cache.
Pattern 3 — Classify, don't dump. The most useful port is not "print all headers" but a decision matrix:
$verdict = match (true) {
str_contains((string) $info['proxy_status'], 'HIT') => 'served-from-proxy',
str_contains((string) $info['proxy_status'], 'MISS') => 'php-hit, stored downstream',
$isPrivateResponse => 'not-cacheable (private)',
default => 'unknown / no proxy',
};
The bundle's value is exactly this kind of label — "is this endpoint safely cacheable?" — and that is more useful than raw header strings for the workflow of "we're about to add personalization; will we break caching?"
Pattern 4 — Verify round-trips, don't just read outbound headers. A miss is often caused by cookies or Cache-Control: no-store injected somewhere upstream. Port the spirit of the bundle's introspection (what is the cache doing) by logging outbound headers on local responses for the same route and comparing with the inbound proxy headers you observe on the next request. This is how you actually confirm a HttpCache/CDN rollout.
Pattern 5 — Gate by environment, not by luck. enabled should be false in production, true in local, and opt-in in staging via an env var. For Laravel 11 use ->appendIf(fn () => config('cache-info.enabled')) in bootstrap/app.php; for Laravel 10 gate inside Kernel::middleware.
Not a Laravel package, full stop. There is no service provider, no facades, no config publish. If you composer require it anyway, expect resolution to fail against a modern Laravel, or — worse — to "succeed" by downgrading symfony/* constraints and then silently do nothing because nothing registers the bundle. That silent no-op is the first failure mode: you get false confidence diagnostics are working. Verify by hitting a route and confirming something is actually written.
Archived, one star, zero dependents = you own it forever. No PR path, no CVE response, no compatibility work. Total cost of ownership for even a single-purpose utility exceeds a ~0.5–1 day Laravel-native middleware. If you install the Symfony bundle into a Laravel app, you are permanently on the hook for an abandoned dependency and its transitive symfony/http-kernel pins.
Composer constraint drift is the sleeper risk. Even if today's install resolves, the next Laravel major will pull Symfony 7 components that won't satisfy this bundle's ^3/^4 pins, and your upgrade is blocked for a feature you could have rewritten in an afternoon. Keep it out of the lockfile.
Never emit diagnostic headers from production paths. Cache keys and hit ratios can leak route structure and personalization state, and — critically — a CDN or intermediate cache can cache X-Cache-Info and serve it to other users. If you must emit a header, pair it with Cache-Control: no-store on that response and restrict to internal traffic. The Symfony profiler implicitly avoided this by only existing in dev, but a header-based port loses that safeguard unless you reimplement the gate.
Fail open. Any middleware that reads headers and logs should be wrapped in try/catch with report(). A null header dereference or a misconfigured header name must never turn a 200 into a 500. Same for the profiler pattern: if the data collector throws during collect(), Symfony's profiler catches it, but the request path does not — don't confuse the two.
Header bloat on high-fanout endpoints. If you log every header on every request, your log volume — not your CPU — becomes the cost. Sample (Str::random style probabilistic) or restrict to a route allowlist once the initial debugging is done.
The bundle's real, portable asset is the mapping, not the code. Its services.yml argument list and its profiler template's field choices tell you what experienced Symfony devs thought was worth showing. Copy those decisions into config/cache-info.php and your verdict labels; ignore the DI/event machinery entirely.
If you genuinely need live HTTP-cache tooling in a Symfony service, do not try to revive this archived bundle. Use maintained FOSHttpCache plus Symfony's documented HttpCache — both are current, documented, and supported. On the CDN side, use the vendor's own analytics; on the APM side, use Telescope/Blackfire/Prometheus instead of per-request introspection in a hot path.
Debugging tip when the proxy verdict looks wrong: compare outbound Vary, Cache-Control, Set-Cookie, and Authorization presence on the same request from php artisan tinker vs through the proxy. Most "wrong cache status" tickets are caused by a cookie or Authorization header being added by front-end middleware, not by the proxy misbehaving — which is exactly the class of issue a cache-info port is meant to surface before it becomes a stale-data ticket.
How can I help you explore Laravel packages today?