PHP code example of vatvit / freshen

1. Go to this page and download the library: Download vatvit/freshen library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

vatvit / freshen example snippets


use Freshen\Key;

$item = $topSellersCache->get(
    new Key('product', 'top-sellers', ['category' => 456, 'brand' => 'Apple'], locale: 'en'),
);

return $item->isMiss() ? [] : $item->value();   // value() returns what the loader produced — here, an array

use Freshen\Driver\Redis as FreshenRedis;

// (a) reuse a \Redis client you already created — one shared connection app-wide
$pool = new \Stash\Pool(              // Stash\Pool: the PSR-6 backend Freshen reads/writes through
    new FreshenRedis([                // Freshen's Redis driver: adds atomic single-flight + exact delete
        'connection' => new \Redis(), //   your already-connected client — you own the socket
    ]),
);

// (b) …or hand the driver connection options and let it open the client for you
$pool = new \Stash\Pool(
    new FreshenRedis(['servers' => [['127.0.0.1', 6379]]]),   // standard Stash-Redis options
);

use Freshen\Cache;
use Freshen\CallableLoader;
use Freshen\DefaultJitter;
use Freshen\Key;

// This loader IS the "top sellers" dataset — the source of truth for that value.
// CallableLoader adapts a plain `fn (Key) => mixed`; in an app you'd inject a Loader service.
$topSellers = new CallableLoader(fn (Key $key) => $repo->topSellers($key->id()));

$topSellersCache = new Cache(
    $pool,
    $topSellers,
    hardTtlSec: 3600,      // absolute lifetime — the entry is gone 3600s after it's written
    precomputeSec: 60,     // in the last 60s before expiry, ONE caller recomputes early
                           //   (stampede-free) while others still read the current value
    jitter: new DefaultJitter(15),   // spread each key's TTL by ±15% so sibling keys
                                     //   don't all expire at the same instant (a stampede cause)
);

$key = new Key(
    'product',                                 // domain — top-level namespace for the entry
    'top-sellers',                             // facet  — the kind of thing within that domain
    ['category' => 456, 'brand' => 'Apple'],   // id     — a scalar OR a map; maps are canonicalised
                                               //          (key order doesn't matter → same key)
    schemaVersion: '2',                        // optional — bump on a value-shape change to
                                               //            invalidate every old entry at once
    locale: 'en',                              // optional — vary the cached entry per locale
);

$result = $topSellersCache->get($key);
if (!$result->isMiss()) {   // isMiss() distinguishes "no entry" from "entry whose value IS null":
                            //   a cached null is a real HIT, so a plain `$value === null` can't tell them apart
    $value = $result->value();
    // $result->isStale() === true while a background recompute is in flight
    $result->createdAt();       // unix seconds the payload was created (null on miss)
    $result->softExpiresAt();   // unix seconds the precompute window opens (null on miss)
}

final class TopSellers                         // a domain object, like a repository
{
    public function __construct(private Cache $cache) {}   // its own per-dataset Freshen\Cache

    /** @return Product[] */
    public function forCategory(int $categoryId, string $locale): array
    {
        $r = $this->cache->get($this->key($categoryId, $locale));
        return $r->isMiss() ? [] : $r->value();
    }

    public function refresh(int $categoryId, string $locale): void
    {
        $this->cache->refresh($this->key($categoryId, $locale));   // recompute via the loader
    }

    private function key(int $categoryId, string $locale): Key
    {
        return new Key('product', 'top-sellers', ['category' => $categoryId], locale: $locale);
    }
}

use Freshen\SyncMode;

$topSellersCache->invalidate($key, SyncMode::SYNC);      // hierarchical: drop the key AND its subtree
$topSellersCache->invalidateExact($key, SyncMode::SYNC); // drop ONLY this key — its subtree (children) stays
$topSellersCache->refresh($key, SyncMode::SYNC);         // recompute now via the loader, then store
$topSellersCache->put($key, $value);                     // store a value you ALREADY have — skips the loader (cheaper than refresh)

$topSellersCache->invalidate($prefix, SyncMode::SYNC);   // e.g. every product/top-sellers/* entry

$topSellersCache->refresh([$key1, $key2], SyncMode::SYNC);

use Freshen\AsyncHandler;
use Freshen\InvalidateEvent;
use Freshen\InvalidateExactEvent;
use Freshen\RefreshEvent;

// build the same cache WITH a PSR-14 dispatcher (Symfony's, League's, …)
$topSellersCache = new Cache(
    $pool, $topSellers,
    hardTtlSec: 3600, precomputeSec: 60, jitter: new DefaultJitter(15),
    eventDispatcher: $dispatcher,
);

// register each event class with its handler method.
// `addListener` here is illustrative — the exact call belongs to YOUR PSR-14
// listener provider (Symfony's, League's, etc.); the routing idea is the same.
$handler = new AsyncHandler($topSellersCache);
$provider->addListener(InvalidateEvent::class,      [$handler, 'handleInvalidation']);
$provider->addListener(InvalidateExactEvent::class, [$handler, 'handleInvalidateExact']);
$provider->addListener(RefreshEvent::class,         [$handler, 'handleRefresh']);

// then, from your request path, fire-and-forget:
$topSellersCache->invalidate($key);        // async (default): dispatches InvalidateEvent
$topSellersCache->invalidateExact($key);   // async (default): dispatches InvalidateExactEvent
$topSellersCache->refresh($key);           // async (default): dispatches RefreshEvent

use Freshen\Interface\MetricsInterface;

$topSellersCache = new Cache(
    $pool, $topSellers,
    hardTtlSec: 3600, precomputeSec: 60, jitter: new DefaultJitter(15),
    metrics: $metrics,   // your MetricsInterface: inc(name, labels) / observe(name, value, labels)
);

$topSellersCache = new Cache(
    $pool, $topSellers,
    hardTtlSec: 3600, precomputeSec: 60, jitter: new DefaultJitter(15),
    failOpen: false,     // prefer an explicit MISS over an uncached recompute
);

use Freshen\{Cache, DefaultJitter, SyncMode};
use Freshen\Driver\Redis as FreshenRedis;

// shared backend — build ONCE, reuse for every cache (don't rebuild the pool per dataset)
$pool   = new \Stash\Pool(new FreshenRedis(['connection' => $redis]));  // $redis: a connected \Redis
$jitter = new DefaultJitter(15);                                        // TTL jitter percent

// one cache PER dataset: its own loader (implements Freshen\Interface\LoaderInterface) + TTLs
$topSellers = new Cache(
    $pool,
    $topSellersLoader,        // your LoaderInterface for THIS dataset
    hardTtlSec: 3600,
    precomputeSec: 60,        // soft window before hard TTL
    jitter: $jitter,
    eventDispatcher: $psr14,  // a PSR-14 dispatcher for async; omit → drive with SyncMode::SYNC
);

$topSellers->get($key);
$topSellers->refresh($key, SyncMode::SYNC);   // no dispatcher? use SYNC; else async is the default
bash
composer 
bash
scripts/php-test.sh       # PHPUnit + PHPStan (level max) across PHP 8.1 → 8.4
scripts/php-coverage.sh   # unit coverage + floor gate
scripts/php-redis-it.sh   # live-Redis integration lane