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 Permanent Cache Laravel Package

backstage/laravel-permanent-cache

A Laravel package for permanent caching: store values indefinitely and retrieve them until explicitly cleared, with a simple API for persistent, long-lived cache entries across your application.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish.

composer require backstage/laravel-permanent-cache
php artisan vendor:publish --tag="permanent-cache-config"

The package is a stale-while-revalidate layer on top of Laravel's cache: entries are written once, served forever from the cache, and refreshed out-of-band so no request ever pays the cost of the expensive operation again.

Version note (v0.3.17). The latest release adds Laravel 12 support. If you are on Laravel 12, make sure Composer actually resolves ^0.3.17 — an older locked 0.3.x may cap the framework constraint and force a conflict or a downgrade of laravel/framework. If you're on Laravel 11 or below, nothing changes for you and any recent 0.3.x will do. There are no breaking API changes in this release; the config file and cacheable contract are untouched.

Configure a store that actually persists. array and null drivers are useless here — the whole premise is that a value survives between requests and between deploys. Use redis, database, memcached, or file (single-server only), and make sure the store is shared across every app server. Set it explicitly in config/permanent-cache.php or point the package at a named store in config/cache.php; don't rely on the default cache store, because another feature's CACHE_DRIVER change will silently reset your permanent entries.

Schedule the refresher. Nothing updates unless something triggers the refresh. The package ships an artisan command that walks the registered cacheables and dispatches a queued job per entry; put it on the scheduler:

// routes/console.php or bootstrap/app.php
Schedule::command('permanent-cache:update')->everyFiveMinutes()->withoutOverlapping();

On Laravel 12 the schedule definition lives in routes/console.php (or bootstrap/app.php via withSchedule()); the command signature is unchanged. Run a queue worker in every environment including staging and production — a refresher command with no worker is a no-op that looks like success.

First use case. Pick the classic one: a dashboard KPI that runs three aggregate queries plus an external API call. Wrap it once, seed it, and watch the page render from a single cache read:

use Backstage\PermanentCache\Facades\PermanentCache;

$stats = PermanentCache::remember('dashboard.kpis', fn () => [
    'orders'   => Order::where('created_at', '>=', now()->subDay())->count(),
    'revenue'  => Order::where('created_at', '>=', now()->subDay())->sum('total'),
    'pipeline' => Http::get(config('services.crm.url'))->json(),
]);

If that renders cold and then never re-computes on the request path, you've understood the package.

Where to look first: the config file (store, TTL/refresh interval, registration list) and the command's source. The config tells you what the package expects; the command tells you exactly when and how entries get rebuilt, which is the only non-obvious behaviour in the whole package.

Implementation Patterns

Class-per-cacheable is the pattern that scales. Closures work for one-off reads, but a background refresher running in a worker cannot re-execute a closure defined in a controller — it has nothing to call. Define each permanent cache as a class implementing the package's cacheable contract, with a stable key and an execute()-style method:

class DashboardKpis implements Cacheable
{
    public function key(): string
    {
        return 'dashboard.kpis';
    }

    public function execute(): array
    {
        return [
            'orders'  => Order::where('created_at', '>=', now()->subDay())->count(),
            'revenue' => Order::where('created_at', '>=', now()->subDay())->sum('total'),
        ];
    }
}

Register it in config/permanent-cache.php and the scheduled command picks it up automatically. This is the difference between a demo and something that survives a week in production.

Keep read paths dumb. Reads should be PermanentCache::get($key) with a fallback to synchronous compute on cold start. Do not put invalidation logic in controllers. The mental model is a materialized view: one write path (the refresher), many readers.

Warm on deploy, not on first request. Add a permanent-cache:update --force (or equivalent) step to your deploy script so the first user after a release doesn't hit an empty cache. This also turns your deploy into the moment when broken cacheables surface — during a controlled window, in CI logs, rather than under traffic.

Pin and test the framework upgrade explicitly. With v0.3.17 the package now spans Laravel 11 and 12, so a framework major bump is a real, tested path — but config/permanent-cache.php is published into your repo and is not re-published on upgrade. After bumping Laravel, run php artisan vendor:publish --tag="permanent-cache-config" --force into a scratch directory and diff it against yours before assuming the new default is a superset of the old one.

Invalidate from model events when correctness matters. Permanent means permanent. If a KPI must be right within seconds of an order being placed, hook Order::saved to delete the key so the next read recomputes synchronously, and let the refresher re-establish the warm copy. Combine: event-driven delete for correctness, scheduled refresh for performance.

Scope keys by tenant or user where data is not global. dashboard.kpis.tenant:{$id} — and register a cacheable per tenant if you want background refresh, or refresh only the tenants touched by the scheduled run. Un-scoped keys are the #1 source of cross-tenant data leaks in cache layers.

Two refresh cadences, not one. Run a fast refresher for cheap, volatile entries (counts, "latest" lists) every minute and a slow one for expensive entries (external API aggregation, report tables) hourly. If the package only supports a single schedule, split registration into two config groups or drive the command manually with a key filter from your own scheduled job.

Test against a real store. Cache::fake()-style assertions verify the call shape but not serialization. Run the interesting tests (phpunit.xml with CACHE_STORE=redis or database) against the real driver at least in CI, because a payload that serializes fine to file can behave differently on Redis with compression or on database with LONGBLOB limits.

Gotchas and Tips

"Permanent" is a data-freshness decision, not a caching decision. You are now responsible for invalidation. Write down, per entry, what makes it wrong and how long staleness is acceptable. Entries without an owner and an invalidation story become zombie data that nobody dares delete.

Closures cannot be refreshed in the background. If your first implementation used remember($key, fn () => ...) inline and the refresher appears to do nothing, this is why. Move to classes before you debug anything else.

A silent queue is the most common failure mode. Symptom: everything works, data is just increasingly old. Check failed_jobs, check the worker is running, check the schedule is actually registered (php artisan schedule:list). Add a monitor — alert if a cacheable's stored updated_at is older than 3× its refresh interval. The package gives you no such alert out of the box.

Version your keys across deploys. If you change the shape of a cached payload, old entries will be read by new code and blow up on array access. Either bump a PERMANENT_CACHE_VERSION env into every key, or ensure the deploy step flushes the namespaced prefix.

Do not store unbounded collections. Redis is memory, and database cache rows are read on every request. A "permanent" cache of 50k Eloquent models will serialize into megabytes and quietly degrade every request. Cache aggregates and IDs; hydrate the rest per request.

Config caching bites. permanent-cache.php is read at boot; after editing registration, run php artisan config:clear locally. In production, config:cache is mandatory and any entry added at runtime won't be seen by the refresher workers.

Debugging checklist, in order: (1) is the key present — Cache::store('permanent')->get($key) directly against the configured store, not the default; (2) is the command finding it — the command usually has a verbose mode or logs each key; (3) is the job dispatched — inspect the queue; (4) did execute() throw — check failed_jobs, since a throwing cacheable often leaves the old value in place and looks like "the refresh didn't run."

Extension points to lean on: the cacheable contract (add a ttl()/shouldRefresh() per entry rather than one global interval), the refresh command (wrap it in your own command to add logging, metrics, and selective keys), and Laravel events — dispatching a PermanentCacheRefreshed-style event from your own wrapper gives you observability without forking.

Maturity risk is real. Six stars, zero dependents, and a recent first release means the API may still move and the bus-factor is one. The v0.3.17 Laravel 12 bump is a genuine patch signal for a package this young (someone is maintaining it), but it is not a stability guarantee. Pin the exact version in composer.json ("backstage/laravel-permanent-cache": "0.3.*" rather than ^0.3), commit composer.lock, read the source of the refresh command before depending on it (it's small), and keep the abstraction thin — a Cacheable class with a key() and execute() is ~15 lines to reimplement if the package is abandoned. Wrap the package facade behind your own thin service class so a swap is a one-file change.

Security note: cached payloads bypass policies and global scopes on read. A key that is global but built from user-scoped data will serve tenant A's numbers to tenant B, and no amount of correct authorization in the controller will catch it because the controller never runs the query. Scope keys at the point of construction, not the point of use.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core