PHPackages                             vatvit/freshen - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Caching](/categories/caching)
4. /
5. vatvit/freshen

Abandoned → [vatvit/freshen-php](/?search=vatvit%2Ffreshen-php)Library[Caching](/categories/caching)

vatvit/freshen
==============

Stale-while-revalidate cache with stampede prevention (single-flight recompute + jittered TTLs).

v1.0.1(1mo ago)087↓83.3%1MITPHP &gt;=8.1

Since Jul 11Pushed 1mo agoCompare

[ Source](https://github.com/vatvit/freshen-php)[ Packagist](https://packagist.org/packages/vatvit/freshen)[ Docs](https://github.com/vatvit/freshen)[ RSS](/packages/vatvit-freshen/feed)WikiDiscussions mirror Synced 1w ago

READMEChangelogDependencies (5)Versions (2)Used By (0)

> ⚠️ **Read-only mirror.** This repository is the Packagist publish mirror of [`packages/php`](https://github.com/vatvit/freshen/tree/main/packages/php) in the [Freshen monorepo](https://github.com/vatvit/freshen). Source, issues, and pull requests live there — please don't open them here. Install with `composer require vatvit/freshen-php`.

---

Freshen (PHP)
=============

[](#freshen-php)

> **Finally, full control over your caching — in one library.**

[![Packagist Version](https://camo.githubusercontent.com/59365a72ed6c44256b211523181b962f9d28a10e57910e6296910ca084c29626/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7661747669742f6672657368656e2d706870)](https://packagist.org/packages/vatvit/freshen-php)[![PHP Version](https://camo.githubusercontent.com/1ef226d93f6f4d29634ac69e950acd9a33ddfe165b15a25d874b05a5181980a9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f7661747669742f6672657368656e2d706870)](https://packagist.org/packages/vatvit/freshen-php)[![License](https://camo.githubusercontent.com/de70a7806e5fa2c5ba0be885dd8af7cd637a904c245b4bde80167147b504f0c5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f7661747669742f6672657368656e2d706870)](https://github.com/vatvit/freshen/blob/main/LICENSE)

Freshen brings together the caching pieces you normally wire up by hand: **single-flight**recompute so exactly one worker rebuilds a hot key while everyone else is served the last good value (**no cache-stampede**); **preemptive refresh** that recomputes an entry *before*it goes stale, on TTLs and jitter you control; **structured keys** and **effective delete**— genuinely evict one exact key or a whole prefix, atomically and in one round-trip; and **built-in metrics** on every hit, miss, and rebuild. Wrap any expensive read — a database query, an API call, a rendered fragment — and every cache-related decision is explicit and yours. Runs natively on **PHP 8.1 → 8.4**([`COMPATIBILITY.md`](https://github.com/vatvit/freshen/blob/main/COMPATIBILITY.md)).

Features
--------

[](#features)

- **Stale-while-revalidate** — serve the cached value instantly and recompute a fresh one in the background; reads never block on an expired entry.
- **Cache-stampede prevention** — single-flight leader/follower recompute plus jittered TTLs: one worker rebuilds while everyone else is served the stale value (no thundering herd).
- **Async invalidation &amp; refresh** — `invalidate()` / `refresh()` are non-blocking by default (PSR-14 events, handled off the call site); pass `SyncMode::SYNC` to run inline.
- **Structured, hierarchical keys** — `Freshen\Key` is `domain / facet [ / schemaVersion ] [ / locale ] / id`, with built-in schema **versioning** and **per-locale** variants.
- **Effective delete** — genuinely evict one exact key, a whole **prefix**(`domain/facet/*`), or a **batch** of selectors — atomically, in a single round-trip (a real delete, not just a TTL bump).
- **Redis-backed, PSR-6 core** — an atomic Redis driver (single-flight + exact/prefix delete) over a Stash PSR-6 pool; swap in any PSR-6 backend.
- **Built-in metrics &amp; fail-open** — hit/miss/recompute metrics out of the box, and it serves through backend hiccups instead of failing the request.
- **Modern, typed PHP** — native PHP 8.1 → 8.4 (single source, no downgrade build), PHPStan-max, MIT.

Framework bridges
-----------------

[](#framework-bridges)

On **Symfony** or **Laravel**? Skip the wiring — a drop-in bridge gives you everything above from declarative config (`composer require` and you're done):

FrameworkPackageDocsSymfony `^6.4 || ^7.0`[`vatvit/freshen-symfony`](https://packagist.org/packages/vatvit/freshen-symfony)[bridge README](../symfony/README.md)Laravel `^11 || ^12` (PHP 8.2+)[`vatvit/freshen-laravel`](https://packagist.org/packages/vatvit/freshen-laravel)[bridge README](../laravel/README.md)At a glance
-----------

[](#at-a-glance)

Once a cache is wired (a few lines — see [Usage](#usage)), reading is two lines and you never touch the store, a stampede, or serialisation:

```
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
```

On a miss, `$topSellersCache` calls your loader, stores the result, and returns it — no "check store → query → write back" dance, no stampede handling. `value()` gives back exactly what the loader returned (an array stays an array; Stash (de)serialises for you), and `isMiss()` even tells a cached `null` apart from an absent entry.

> This directory is the source of truth. It is subtree-split by CI into a read-only mirror repository that Packagist serves.

Install
-------

[](#install)

```
composer require vatvit/freshen-php
```

Requires a [PSR-6](https://www.php-fig.org/psr/psr-6/) cache pool ([Stash](https://github.com/tedious/Stash)). `ext-redis` is *suggested* for a Redis backend.

Usage
-----

[](#usage)

### 1. Wire a backend

[](#1-wire-a-backend)

`Freshen\Cache` reads and writes through a [Stash](https://github.com/tedious/Stash)pool. For the **full** guarantees (atomic single-flight recompute and **exact**, non-hierarchical invalidation) use Freshen's Redis driver, `Freshen\Driver\Redis`, in place of `Stash\Driver\Redis`. Two equivalent ways to build the pool:

```
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
);
```

`$pool` is a plain **PSR-6** pool (`\Stash\Pool` implements `Psr\Cache\CacheItemPoolInterface`) — Freshen *consumes* it, it isn't a Freshen type; if you already cache with Stash it's the pool you already have. `Cache` wires its own item class (`Freshen\Item`, for deterministic TTLs and exact delete) onto the pool automatically — you don't set it yourself. Strong single-flight needs a backend with a conditional write: **Redis** (`SET NX`) today; other Stash drivers work but fall back to Stash's best-effort lock (see [`docs/PARITY.md`](https://github.com/vatvit/freshen/blob/main/docs/PARITY.md) §12, §14).

### 2. Build a cache for one dataset, and read

[](#2-build-a-cache-for-one-dataset-and-read)

A `Freshen\Cache` is **not a global cache** — it wraps **one loader**, i.e. one dataset, with its own TTLs. The loader is the heart of the library: Freshen calls it to (re)compute the authoritative value for a key (your DB query, an API call, a heavy computation). **On a read you never write values yourself** — a `get()`on a cold or due key invokes the loader and Freshen stores the result. That's the whole point: *read, and the cache keeps itself fresh, stampede-free.*

Need another dataset (say, categories)? That's **another loader and another `Cache`**, with its own TTLs — see [Framework integration](#framework-integration)for the per-dataset wiring.

```
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)
);
```

A **`Key`** is a structured, immutable identity — `domain / facet [ / schemaVersion ] [ / locale ] / id`:

```
$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
);
```

**Why `domain` + `facet`?** Together they form the key's **prefix** — a hierarchy, not just a namespace. `domain` is the bounded context / entity type (`product`, `user`, `order`); `facet` is the specific view or query within it (`top-sellers`, `profile`, `detail`). Grouping entries under a shared prefix is what makes **hierarchical invalidation** work: `invalidate()` on a prefix drops *every* entry beneath it in one call (e.g. clear all `product/top-sellers/*` variants at once), while `invalidateExact()` removes just the single entry.

**Why the `id` can be complex (a map).** A cached value is usually a function of *several* inputs — filters, pagination, options — not one scalar. Rather than make you hand-build and normalise a string, `Key` takes the whole parameter map and **canonicalises** it: keys are deep-sorted so logically-equal inputs produce the *same* key regardless of order (`['brand' => 'Apple', 'category' => 456]` is the same key as the example above), then serialised to a deterministic, separator-safe, cross-language-stable token. So multi-dimensional keys stay correct and collision-free with no effort on your side.

Now just **read**. On a miss the loader fills the cache; within the precompute window one caller refreshes while the rest are served the current value; under a recompute a follower is served the previous (stale) value — all automatic:

```
$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)
}
```

`value()` throws a `RuntimeException` on a miss — guard with `isMiss()` (or `isHit()` / `isStale()`) first.

> **To update an entry, pick by whether you already hold the value.** If you *don't*have it, drive the loader: `invalidate()` drops the entry (next `get()` recomputes) and `refresh()` recomputes it now (§3/§4). If you *already* have a fresh value — you just computed it, or a write path produced it — `put($key, $value)` stores it directly and **skips the loader**; using `refresh()` there would waste a recompute.

### A cache is a domain object — wrap it like a repository

[](#a-cache-is-a-domain-object--wrap-it-like-a-repository)

This is the architectural intent of the class layout: a `Freshen\Cache` instance is **the complete logic for one piece of business data** — how it's loaded (the loader), how long it lives (TTLs), how it's refreshed and invalidated. It is *not* a generic key-value bucket you reach into from everywhere.

You already do this for the database: raw SQL doesn't get sprinkled across the app, it's wrapped in a `ProductRepository`. **Cached data is no different** — so isolate each dataset behind a small business object that owns its cache and hides the keys:

```
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);
    }
}
```

Callers write `$topSellers->forCategory(456, 'en')` — they never touch a `Key`, a TTL, or the word "cache". Each dataset (top sellers, categories, a user profile) is its own such object with its own `Cache`, loader, and TTLs. **Split and isolate your cached data**; treat it as the first-class domain concept it is.

### 3. Invalidate &amp; refresh

[](#3-invalidate--refresh)

Three write-side operations. Each defaults to **async** (§4); pass `SyncMode::SYNC`to act inline against the backend:

```
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)
```

**Prefix selector.** `invalidate()` also accepts a `Freshen\Interface\KeyPrefixInterface`— clear a whole subtree without a concrete `Key`:

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

**Batching.** All three accept a **list** to act on many selectors in one call:

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

### 4. Invalidate &amp; refresh — asynchronous (the default)

[](#4-invalidate--refresh--asynchronous-the-default)

`invalidate()` / `invalidateExact()` / `refresh()` default to `SyncMode::ASYNC`: instead of touching the backend inline they emit a per-operation event through a [PSR-14](https://www.php-fig.org/psr/psr-14/) dispatcher, and a subscribed `Freshen\AsyncHandler` performs the equivalent **SYNC** operation later. Each operation has its own event class — `Freshen\InvalidateEvent`, `Freshen\InvalidateExactEvent`, `Freshen\RefreshEvent` (all extend the abstract `Freshen\AsyncEvent`) — so a listener provider routes each op to the right handler by event class alone. Give the cache a dispatcher, then wire the handler:

```
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
```

Calling an async operation when no dispatcher was configured throws a `LogicException`.

### 5. Observability — metrics, built in

[](#5-observability--metrics-built-in)

Freshen emits a **named metric on every read/write path**, so hit / stale / fill / miss / invalidate visibility is built in, not bolted on. Implement `MetricsInterface`to forward them to StatsD, Prometheus, your logger — anything. No sink? Metrics are simply off, at zero cost.

```
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)
);
```

Emitted set: `cache_hit{state: fresh|stale|fresh_after_sleep}`, `cache_fill`, `cache_put`, `cache_miss{cause: …}`, `cache_invalidate`, `cache_invalidate_hierarchical`. Fire-and-forget — a sink **must not** throw into the cache path. See [`docs/PARITY.md`](https://github.com/vatvit/freshen/blob/main/docs/PARITY.md) §10.

### 6. Fail-open

[](#6-fail-open)

`failOpen` (constructor, default `true`) is the last-resort behaviour under contention when there is no value to serve: `true` recomputes via the loader and returns it **without caching** (a `HIT`) — availability over a cold store; `false`returns a `MISS` instead. See [`docs/PARITY.md`](https://github.com/vatvit/freshen/blob/main/docs/PARITY.md) §7.

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

### How it wires into your app (PSR + DI)

[](#how-it-wires-into-your-app-psr--di)

Freshen ships **no framework of its own** — it's a small core that plugs into standards you already use, so wire it like any other service:

- **Backend** — a **PSR-6** pool (Stash). Freshen consumes the pool you hand it; it bundles no store and forces no cache abstraction on you.
- **`Loader`, `Jitter`, `Metrics`** — plain interfaces (`Freshen\Interface\{LoaderInterface, JitterInterface, MetricsInterface}`). `CallableLoader` and `DefaultJitter` are bundled *defaults* for a quick start; in a real app you implement (or bind) these and **inject them via your DI container**like anything else. The constructor takes the interfaces, never a concrete.
- **Async events &amp; handler** — `AsyncEvent` / `InvalidateEvent` / `InvalidateExactEvent` / `RefreshEvent` and `AsyncHandler` are plain PHP objects with **no dispatcher of their own**. They travel through **any PSR-14 `EventDispatcherInterface`** — Symfony's, League's, your framework's — so Freshen reuses your existing event bus instead of shipping one. The one hard rule: async ops need *a* dispatcher wired in (else `LogicException`).

Framework integration
---------------------

[](#framework-integration)

Prefer a **[drop-in bridge](#framework-bridges)** on Symfony or Laravel — it wires all of this for you. Three principles hold whichever path you take: (1) Freshen needs a **Stash** pool (not the framework's own PSR-6 pool); (2) async needs a **PSR-14** dispatcher — **Symfony's is**PSR-14, **Laravel's is not** (its bridge ships a PSR-14 adapter + queue); and (3) **a `Cache` is per-dataset** — one loader, its own TTLs. A second dataset is a second loader + a second cache, each with its own config.

If you're **not** on those frameworks (or want to wire it by hand), see [Manual wiring](#manual-wiring) below.

### Manual wiring

[](#manual-wiring)

On Symfony or Laravel, prefer the **bridge** (above) — it does all of this for you. Wire it by hand only for another framework, a plain PSR-6 setup, or full control. A `Cache` composes four things; build them as shared services and inject a cache **per dataset**:

```
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
```

For async invalidation, register one `Freshen\AsyncHandler($cache)` per cache on a PSR-14 dispatcher, routing each event class to its method (`InvalidateEvent → handleInvalidation`, `InvalidateExactEvent → handleInvalidateExact`, `RefreshEvent → handleRefresh`). **Symfony's `event_dispatcher` is PSR-14; Laravel's is not** — which is exactly what the bridges handle (Symfony natively, Laravel via a PSR-14 adapter + queue). A second dataset is a second loader + a second `Cache` reusing the same shared pool.

Escape hatch &amp; limitations
------------------------------

[](#escape-hatch--limitations)

`$cache->asPool()` exposes the underlying Stash [PSR-6](https://www.php-fig.org/psr/psr-6/) pool for advanced/host use. Note that **whole-store clear is intentionally unsupported**: `$cache->asPool()->clear()` throws a `RuntimeException`. Stash's pool-wide clear maps to Redis `FLUSHDB`, which wipes the entire database — every key, not just cached ones — so Freshen does not expose it. Clear cached data by key or prefix with `invalidate()` / `invalidateExact()` instead.

The cross-language behaviour contract is [`docs/PARITY.md`](https://github.com/vatvit/freshen/blob/main/docs/PARITY.md).

Security
--------

[](#security)

Tracked by the [Packagist security advisory database](https://packagist.org/packages/vatvit/freshen-php)— run `composer audit` to check your install. Report vulnerabilities privately via [GitHub Security Advisories](https://github.com/vatvit/freshen/security/advisories); the full policy is in [SECURITY.md](https://github.com/vatvit/freshen/blob/main/SECURITY.md).

Develop / contribute
--------------------

[](#develop--contribute)

The test and quality tooling runs across the full PHP version matrix in **Docker**(each script spins up the right container) — install Docker, then from the repo root:

```
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
```

If you have PHP 8.1+ and Composer locally, you can also run the suite directly inside `packages/php`:

```
composer install && composer test
```

License
-------

[](#license)

[MIT](https://github.com/vatvit/freshen/blob/main/LICENSE)

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance90

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

46d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/aa497f3614944072f8d2ba5e4d6411f7a278564aaa1db11e42f1287e9963fc73?d=identicon)[vatvit](/maintainers/vatvit)

---

Top Contributors

[![vatvit](https://avatars.githubusercontent.com/u/1484520?v=4)](https://github.com/vatvit "vatvit (2 commits)")

---

Tags

rediscachepsr-16psr-6swrstale-while-revalidatestampede

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/vatvit-freshen/health.svg)

```
[![Health](https://phpackages.com/badges/vatvit-freshen/health.svg)](https://phpackages.com/packages/vatvit-freshen)
```

###  Alternatives

[symfony/symfony

The Symfony PHP framework

31.4k87.4M2.2k](/packages/symfony-symfony)[symfony/mailer

Helps sending emails

1.6k424.2M1.7k](/packages/symfony-mailer)[drupal/core-recommended

Locked core dependencies; require this project INSTEAD OF drupal/core.

6943.5M450](/packages/drupal-core-recommended)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

595.8M684](/packages/shopware-core)[eliashaeussler/typo3-warming

Warming - Warms up Frontend caches based on an XML sitemap. Cache warmup can be triggered via TYPO3 backend or using a console command. Supports multiple languages and custom crawler implementations.

22272.4k](/packages/eliashaeussler-typo3-warming)[eliashaeussler/cache-warmup

Composer package to warm up website caches, based on a given XML sitemap

77449.4k12](/packages/eliashaeussler-cache-warmup)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
