vatvit/freshen-laravel

Laravel bridge for Freshen — drop-in stale-while-revalidate cache with stampede prevention.

Maintainers

Package info

github.com/vatvit/freshen-laravel

Homepage

Issues

pkg:composer/vatvit/freshen-laravel

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0-rc.1 2026-07-11 09:42 UTC

This package is auto-updated.

Last update: 2026-07-11 09:48:27 UTC


README

⚠️ Read-only mirror. This repository is the Packagist publish mirror of packages/laravel in the Freshen monorepo. Source, issues, and pull requests live there — please don't open them here. Install with composer require vatvit/freshen-laravel.

Freshen — Laravel bridge

Packagist Version PHP Version License

Security — tracked by the Packagist security advisory database; run composer audit to check your install. Report privately via GitHub Security Advisories; policy in SECURITY.md.

vatvit/freshen-laravel is the drop-in Laravel package for Freshen, the stale-while-revalidate cache with stampede prevention. It wires the manual pool/loader/listener setup from the core README into a service provider + config file: composer require, publish the config, define one cache per dataset, and resolve them by name — with async invalidation already on the queue.

Install

composer require vatvit/freshen-laravel

The FreshenServiceProvider is auto-discovered (package discovery). Publish the config:

php artisan vendor:publish --tag=freshen-config

Configure

A Freshen cache is one dataset — its own loader + TTLs — so a real app defines one cache per data structure (top sellers, prices, categories, …). You declare each under caches in config/freshen.php, keyed by the name you'll resolve it with. Each references a loader (yours, implementing Freshen\Interface\LoaderInterface) resolved from the container, and a Laravel redis connection name whose phpredis client Freshen reuses.

// config/freshen.php
return [
    'queue' => [
        'connection' => env('FRESHEN_QUEUE_CONNECTION'), // null = default; 'sync' = inline
        'queue' => env('FRESHEN_QUEUE'),                 // null = default queue name
    ],

    // one entry per dataset — the key is the name you pass to Freshen::cache('<name>')
    'caches' => [
        'top_sellers' => [
            'loader' => App\Cache\TopSellersLoader::class, // required — LoaderInterface
            'hard_ttl' => 3600,                            // required, seconds (>= 1)
            'precompute' => 60,                            // default 0 — soft window
            'jitter' => 15,                                // default 15 (percent)
            'fail_open' => true,                           // default true
            'connection' => 'default',                     // Laravel redis connection name
            // 'metrics' => App\Cache\Metrics::class,      // optional — MetricsInterface
        ],
        'prices' => [
            'loader' => App\Cache\PricesLoader::class,
            'hard_ttl' => 600,
            'precompute' => 30,
        ],
    ],
];

Freshen reuses the phpredis client from config/database.php's redis.<connection> — no second connection is opened. Use the phpredis client ('client' => 'phpredis').

Use

Resolve a cache by name with the Freshen facade — there is no "default" cache, since each one is a distinct dataset:

use Freshen\Bridge\Laravel\Facades\Freshen;
use Freshen\Key;

$key    = new Key('product', 'detail', $id);
$result = Freshen::cache('top_sellers')->get($key);
$result->value();                             // fresh or stale-while-revalidate value

Freshen::cache('top_sellers')->invalidate($key);   // async by default — see below

Prefer constructor injection? Inject the manager and pick the dataset:

use Freshen\Bridge\Laravel\FreshenManager;

public function __construct(private FreshenManager $freshen) {}

$this->freshen->cache('prices')->get($key);

(You inject FreshenManager (or use the Freshen facade), never a bare Freshen\Cache: with several datasets Laravel can't autowire Freshen\Cache by type, so it isn't bound — you always ask for a cache by name.)

Async invalidation (queue)

Laravel's event dispatcher is not PSR-14, so the bridge ships a small PSR-14 adapter that pushes async operations onto Laravel's queue. invalidate() / invalidateExact() / refresh() (async by default) dispatch a ProcessFreshenAsyncEvent job that runs the cache's Freshen\AsyncHandler on a worker — off the request.

use Freshen\Bridge\Laravel\Facades\Freshen;
use Freshen\Key;

// e.g. a model observer: when a product changes, drop its cached view
class ProductObserver
{
    public function saved(Product $product): void
    {
        $key = new Key('product', 'detail', $product->id);

        Freshen::cache('top_sellers')->invalidate($key);   // async → enqueues a job
        // Freshen::cache('top_sellers')->refresh($key);    // async recompute + store
    }
}
php artisan queue:work        # a worker runs the invalidation off the request

Need a specific call to run inline (no worker)? Pass SyncMode::SYNC:

use Freshen\SyncMode;

Freshen::cache('top_sellers')->invalidate($key, SyncMode::SYNC);   // runs now, skips the queue
  • Configure the connection/queue via config/freshen.php queue (or the FRESHEN_QUEUE_CONNECTION / FRESHEN_QUEUE env vars).
  • Set the connection to sync to run all of a cache's invalidations inline (no worker).
  • Run a worker for true off-request async: php artisan queue:work.

Each job carries its target cache name, so async invalidation is routed to exactly the right cache (no cross-cache fan-out).

Versioning

Independent SemVer, released as laravel-vX.Y.Z. Depends on vatvit/freshen-php via ^1.0@rc (→ ^1.0 once core is stable). A core patch/minor needs no bridge release; a core major does. See the monorepo RELEASING.md.

Requires PHP 8.2+ and Laravel 11 or 12. (Laravel 10 — the only 8.1-compatible line — is EOL and flagged by composer security advisories, so it is not supported.)

Links