ryangjchandler/blade-cache-directive
A lightweight Laravel package that adds an @cache Blade directive, letting you cache rendered fragments of views—partials, loops, or expensive blocks—with a TTL and optional key. Drop it into existing templates to cut rendering time without extra setup.
Install via Composer — the service provider is auto-discovered, so there is nothing to register:
composer require ryangjchandler/blade-cache-directive
As of v1.1.1 the package declares Laravel 13.x compatibility, so the constraint resolves cleanly on Laravel 10/11/12/13. If you are pinned to an older release, bump to ^1.1.1 before upgrading your app skeleton — earlier tags will fail composer update on a Laravel 13 host.
Where to look first:
config/cache.php — the directive uses your default cache store, so the driver (file, redis, database, array) determines persistence and invalidation behaviour.php artisan view:clear then load a page and inspect storage/framework/views/*.php to see exactly what @cache compiles into. This is the fastest way to understand the semantics.First use case — wrap an expensive fragment (a menu query, a computed widget, a slow relationship loop):
@cache('header-navigation')
@foreach (app(Navigation::class)->tree() as $item)
<a href="{{ $item->url }}">{{ $item->label }}</a>
@endforeach
@endcache
The second argument is a TTL in seconds. Omit it and the fragment is cached indefinitely until you clear it:
@cache('homepage.stats', 300)
{{-- refreshed every 5 minutes --}}
@endcache
Dynamic keys. The key is a normal PHP expression, so build it from whatever distinguishes the fragment:
@cache("post.{$post->id}.v{$post->updated_at->timestamp}")
@include('partials.post-body', ['post' => $post])
@endcache
Per-user scoping. The directive has no notion of the current user. If the fragment varies by auth state, put that in the key yourself:
@cache('cart.' . (auth()->id() ?? 'guest'))
...
@endcache
Version-key busting for deploy-time invalidation. Because there is no automatic invalidation, a common pattern is to namespace every key with a config value you bump on deploy:
// config/view.php or a dedicated config/cache-fragments.php
'version' => env('FRAGMENT_CACHE_VERSION', '1'),
@cache('v' . config('cache-fragments.version') . '.homepage.hero')
Model-driven invalidation. Clear fragments explicitly from observers when the underlying data changes:
class PostObserver
{
public function saved(Post $post): void
{
Cache::forget("post.{$post->id}.v{$post->updated_at->timestamp}");
Cache::forget('homepage.latest-posts');
}
}
For anything with many keys, prefer short TTLs over elaborate invalidation logic — it is far less error-prone.
Wrapping includes and components. @cache around @include / <x-…> works and is the cleanest way to keep markup readable:
@cache('sidebar.trending', 600)
<x-trending-posts :limit="5" />
@endcache
Nesting. Nested @cache blocks are supported, but each nested block triggers its own cache read and its own output buffer. Distinct keys are mandatory; nesting is usually a sign you should cache the inner fragment only.
Integration tip — testing. In tests the default store is often array, so caching happens but does not survive the request. Assert on rendered output and use Cache::spy() / Cache::shouldReceive('remember') when you want to prove the fragment was served from cache:
Cache::spy();
$this->blade('@cache("k", 60) hello @endcache')->assertSee('hello');
Cache::shouldHaveReceived('put')->once();
You can also render a directive in isolation with Blade::render() for fast unit-level checks.
@cache('sidebar') in two different layouts returns the first rendered variant to both. Prefix keys with the route, layout, or model: layout.sidebar, not sidebar.@cache('row') inside @foreach ($items as $item) caches the first row and replays it for every subsequent item. Use @cache("row.{$item->id}").@php assignments, counters, @push to stacks) only execute on a cache miss. Never put stack pushes, script registration, or state mutation inside @cache — push outside the block instead.{!! !!} output for a user whose permissions changed), you are serving a stale authorisation result. Keep permission-dependent fragments out of long-lived caches.file and database have no tag support; Cache::tags()->flush() cannot free fragments created by this directive. Design for Cache::forget($key) or TTLs. array in tests means every request is a cold cache.@cache('frag.' . md5($signature)).cache()->has($key) in a tinker session, or temporarily add {{-- cached --}} markers. php artisan cache:clear nukes everything — on Redis, prefer targeted Cache::forget() calls in a Tinker session.@cache is outside the block; one immediately inside is inside it and becomes part of the cached string.php artisan view:clear as well — otherwise you may be looking at stale compiled Blade and a stale cache entry and misdiagnose which one is at fault.Cache::remember() with the strategy you want, and keep this package for the simple cases.composer.json allows ^1.1.1 and re-run your view tests after the framework upgrade.How can I help you explore Laravel packages today?