Components · v1.x

Cache

The Cache component stores computed values in named pools: filesystem, APCu, database or in-memory adapters, TTL, tags, and get() with a callback protected against cache stampedes. No external library is used. The HttpClient can store its responses in a pool (see HttpClient integration).

Quick start

use NeoPHP\Component\Cache\Contract\CacheInterface;

class WeatherService
{
    public function __construct(protected CacheInterface $cache, protected WeatherApi $api)
    {
    }

    public function today(string $city): array
    {
        return $this->cache->get('weather.' . md5($city), fn (): array => $this->api->fetch($city), 600);
    }
}

The first call runs the callback and stores its result for 600 seconds; the next calls read the stored value. Injecting CacheInterface gives the default pool.

Configuration

config/framework/cache.yaml (generated by the installer):

default_pool: app

pools:
  app:
    adapter: filesystem
    default_ttl: 3600

A complete example:

default_pool: app

pools:
  app:
    adapter: filesystem
    default_ttl: 3600                              # seconds, ~ = forever
    directory: '%kernel.root_path%/var/cache/pools/app'
  fast:
    adapter: apcu
    namespace: myapp                               # key prefix (default: the pool name)
    default_ttl: 300
  shared:
    adapter: database
    connection: default                            # connection of config/framework/database.yaml (default connection when omitted)
    table: cache_items
    default_ttl: ~
  runtime:
    adapter: array
Pool option Default Description
adapter filesystem filesystem, apcu, database or array
default_ttl ~ TTL used when set() / get() receive none; ~ = no expiration
namespace pool name prefix of the keys, so that pools sharing a storage do not collide
lock_timeout 5 seconds a process waits for the lock of computed values
directory %kernel.root_path%/var/cache/pools/<pool> filesystem only; a relative path is relative to the project root
connection default connection database only
table cache_items database only

A pool can also be written as a string: runtime: array.

Without cache.yaml, one pool exists: app, filesystem, default_ttl: 3600.

Adapters

Adapter Storage Notes
filesystem one file per item, in sub-directories named after the hash of the key writes are atomic (temporary file + rename()); the expiration timestamp is the first line of the file; locks with flock() in .locks/
apcu shared memory of APCu requires ext-apcu; creating the pool throws a CacheException when the extension is missing or disabled (set apc.enable_cli=1 to use it in the console); locks with apcu_add()
database a table of a Database connection the table is created on first use (MySQL / MariaDB, SQLite, PostgreSQL); values are serialized then base64 encoded; locks are rows inserted in the same table
array PHP memory lost at the end of the request; useful for tests and per-request memoization

The table of the database adapter:

CREATE TABLE IF NOT EXISTS cache_items (
    item_key VARCHAR(255) NOT NULL PRIMARY KEY,
    item_value TEXT NOT NULL,          -- LONGTEXT on MySQL / MariaDB
    expires_at INTEGER NULL
);

DatabaseAdapter::createTable() creates it explicitly (for example in a migration).

Reading and writing

$cache->set('user.42', $user);                       // default TTL of the pool
$cache->set('user.42', $user, 60);                   // 60 seconds
$cache->set('user.42', $user, new DateInterval('PT1H'));
$cache->get('user.42');                              // value, or null when missing
$cache->has('user.42');
$cache->delete('user.42');

$cache->setMany(['a' => 1, 'b' => 2], 300);
$cache->getMany(['a', 'b', 'c'], 'default');          // ['a' => 1, 'b' => 2, 'c' => 'default']
$cache->deleteMany(['a', 'b']);

$cache->clear();                                     // every item of the pool
  • Values are stored with serialize(): scalars, arrays and serializable objects are supported (not closures or resources). null can be stored: use has() to distinguish it from a missing key.
  • A TTL is an integer (seconds) or a DateInterval; a TTL of 0 or less deletes the item.
  • Keys cannot be empty and cannot contain {}()/\@:; an invalid key throws NeoPHP\Component\Cache\Exception\InvalidArgumentException. Use dots or underscores: user.42, weather_paris.

Computing values

get() with a callback returns the stored value, or runs the callback, stores its result and returns it:

use NeoPHP\Component\Cache\Item\CacheItem;

$products = $cache->get('products.home', function (CacheItem $item) use ($repository): array {
    $products = $repository->findFeatured();
    $item->expiresAfter(count($products) > 0 ? 3600 : 60);
    $item->tag(['products']);

    return $products;
}, 600, ['home']);

The callback receives a CacheItem: getKey(), expiresAfter(int|DateInterval|null) and tag(string|array) change the TTL and the tags given to get() (third and fourth arguments). The argument can be omitted: fn (): array => ... works too.

Stampede protection: when many requests miss the same key at the same time, only one computes it. The process that misses takes a lock on the key (flock() for filesystem, apcu_add() for APCu, a lock row for database), reads the key again, and computes the value only if it is still missing. The other processes wait for the lock, then read the value stored by the first one. When the lock cannot be taken within lock_timeout seconds (5 by default), the process computes the value without lock. The array adapter does not lock (it is local to the process).

Tags

Tags invalidate groups of items without knowing their keys:

$cache->set('product.1', $product, 3600, ['products', 'category.4']);
$cache->get('category.4.list', fn (): array => $repository->byCategory(4), 3600, ['category.4']);

$cache->invalidateTags(['category.4']);   // product.1 and category.4.list become misses

Each tag has a version stored in the pool; an item keeps the versions of its tags when it is written. invalidateTags() changes the versions, so the items written before are read as misses (and are overwritten or pruned later). Tag names follow the key rules.

Expiration and pruning

Expired items are never returned. APCu removes them itself; the array adapter drops them on read. The filesystem and database adapters delete an expired item when it is read, and prune() removes all the expired items of a pool:

$removed = $cache->prune();
php bin/neo cache:pool:prune            # schedule it with cron

Named pools

use NeoPHP\Component\Cache\Contract\CacheInterface;
use NeoPHP\Component\Cache\Contract\CacheManagerInterface;
use NeoPHP\Component\Container\Attribute\Autowire;

class ReportService
{
    public function __construct(
        protected CacheManagerInterface $caches,
        #[Autowire(service: 'cache.shared')] protected CacheInterface $shared,
    ) {
    }

    public function build(): array
    {
        return $this->caches->pool('fast')->get('report', fn (): array => $this->compute());
    }
}
Service Value
CacheManagerInterface, CacheManager, CacheInterface, cache the manager; used as CacheInterface, it delegates to the default pool
cache.<pool> the pool <pool>

CacheManagerInterface: pool(?string $name = null), hasPool(), getPoolNames(), getDefaultPool(), getPoolConfig(). An unknown pool throws a CacheException listing the configured pools.

Controllers

AbstractController provides cache():

public function index(): Response
{
    $stats = $this->cache()->get('stats', fn (): array => $this->computeStats(), 300);
    $top = $this->cache('fast')->get('top', fn (): array => $this->computeTop());

    return $this->render('home/index', ['stats' => $stats, 'top' => $top]);
}

cache() returns the default pool, cache('name') a named pool.

Console

Command Description
cache:pool:list pools with their adapter, default TTL and namespace
cache:pool:clear [pools]... [--all] clears pools (asks which one when none is given)
cache:pool:delete <pool> <key> deletes one item
cache:pool:invalidate-tags <tags>... [--pool=] invalidates tags in every pool, or only in the --pool ones (repeatable)
cache:pool:prune [pools]... removes the expired items (every pool by default)

cache:clear (Kernel) is different: it deletes the files of var/cache/ (routes, templates, container...). It does not know the pools: APCu and database pools are not cleared by it, while filesystem pools stored in var/cache/pools/ (the default directory) are removed with the rest of var/cache/. Use cache:pool:clear to clear pools whatever their adapter.

HttpClient integration

The cache option of the HttpClient stores GET / HEAD responses in a pool, honors Cache-Control and revalidates stale responses with ETag / Last-Modified:

$http->get('https://api.example.com/countries', ['cache' => true]);
$http->get('https://api.example.com/countries', ['cache' => ['pool' => 'shared', 'ttl' => 3600]]);

See Caching in the HttpClient documentation.

CacheInterface reference

Method Description
getName(): string pool name
getAdapter(): AdapterInterface storage adapter
get(string $key, ?callable $callback = null, int|DateInterval|null $ttl = null, array $tags = []): mixed value; with a callback, computes and stores it when missing
set(string $key, mixed $value, int|DateInterval|null $ttl = null, array $tags = []): bool stores a value
has(string $key): bool the key exists and is not expired or invalidated
delete(string $key): bool deletes an item
getMany(array $keys, mixed $default = null): array values indexed by key
setMany(array $values, int|DateInterval|null $ttl = null, array $tags = []): bool stores several values
deleteMany(array $keys): bool deletes several items
clear(): bool deletes every item of the pool
invalidateTags(array $tags): bool invalidates the items having one of these tags
prune(): int removes the expired items, returns their number

Custom storages implement Contract\AdapterInterface (usually by extending Contract\AbstractAdapter) and are wrapped in a CachePool: new CachePool('name', $adapter, 3600).

Changelog

  • v1.22.0 — Cache component: CacheInterface (get() with callback and stampede protection, set(), has(), delete(), getMany() / setMany() / deleteMany(), clear(), invalidateTags(), prune()), CacheManager with named pools cache.<pool>, filesystem / APCu / database / array adapters, tags, cache() in controllers, cache:pool:list / cache:pool:clear / cache:pool:delete / cache:pool:invalidate-tags / cache:pool:prune commands, config/framework/cache.yaml.