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
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
Loading please wait ...
Before you can download the PHP files, the dependencies should be resolved. This can take some minutes. Please be patient.