tempest/kv-store
A lightweight key-value storage abstraction for Tempest PHP applications, offering a simple, consistent API for storing, retrieving, and managing persisted values while keeping storage code clean and framework-friendly.
The package is a read-only subsplit of the KV-store component that lives in the tempestphp/tempest-framework monorepo. That single fact shapes everything about how you work with it.
Install. If you already use Tempest, the component ships with the framework — don't add it twice. Standalone:
composer require tempest/kv-store
Check composer.json first for the PHP constraint (Tempest currently targets a modern PHP, 8.3/8.4-era) and for its tempest/container / tempest/core dependencies. A component subsplit is not a framework; you'll need the container and discovery packages to use it outside Tempest.
Read the interface before anything else. Open src/ and find the contract — this is the only file that matters for day-to-day work. Look for the interface (e.g. KeyValueStore) and the concrete implementations beside it (in-memory, file, and possibly a Redis/array adapter). Note the exact signatures for get/set/delete/has and whether a TTL argument exists. Every pattern below hinges on those signatures, so verify them rather than assuming Laravel Cache parity.
Find the binding. Because Tempest uses attribute-driven discovery, the interface is bound to a default implementation somewhere — a config class, an initializer, or a #[Singleton] attribute on the implementation. That binding is your extension point for swapping stores per environment.
First use case: cache-aside an expensive read. Inject the store and guard a costly computation:
use Tempest\KeyValue\KeyValueStore;
final readonly class StatsController
{
public function __construct(private KeyValueStore $store) {}
#[Get('/stats')]
public function __invoke(): array
{
$key = 'app:stats:v1';
if (($cached = $this->store->get($key)) !== null) {
return $cached;
}
$stats = $this->recompute(); // expensive
$this->store->set($key, $stats);
return $stats;
}
}
Confirm the actual method names/signatures in the interface; adjust the null-check accordingly if the contract uses a default-value parameter.
Wrap the contract in a typed service. Raw string keys scattered across controllers is how KV stores rot. Create one small class per domain concern that owns the keys, the serialization shape, and the TTL:
final readonly class StatsCache
{
public function __construct(private KeyValueStore $store) {}
public function get(): ?array { /* key + envelope logic */ }
public function put(array $stats): void { /* ... */ }
public function flush(): void { /* ... */ }
}
Inject that, not the store, into application code. Swapping the backing store later then touches one file.
Namespace every key. Treat the store as one flat, shared namespace: app:{domain}:{version}:{id}. The version segment is your poor-man's cache-tag invalidation — bump it to invalidate a whole domain without enumerating keys (KV stores rarely offer flushByPrefix, let alone Laravel-style tags).
Roll your own remember() and TTL if absent. Laravel's Cache::remember has no direct equivalent here at the contract level. Either the adapter supports per-key expiry, or you store an envelope:
['value' => $value, 'expiresAt' => time() + 300]
and treat expiresAt as authoritative on read. Pick one approach and centralize it in the wrapper service — mixing both is a debugging nightmare.
Swap to in-memory for tests. Bind the in-memory implementation in your test bootstrap so unit tests never touch the filesystem or a network store. This is the single highest-value integration; it's also the reason to keep the interface as the only thing you type-hint.
High-leverage integration points in a Tempest app:
set('webhook:{eventId}', true) before processing; skip on replay.Laravel mental mapping. Cache::get/put/forget → the store's get/set/delete; Cache::remember → your wrapper; cache tags → versioned key prefixes; Cache::lock → not available unless the adapter documents atomicity (see below).
[READ ONLY]. Issues and PRs go to tempestphp/tempest-framework. Don't open a PR against the subsplit — it will be closed.composer.json and keep the store behind your wrapper interface so a signature change is a one-file fix.#[ConsoleCommand]) that walks the backing directory or scans keys and deletes expired envelopes — and schedule it.KEYS app:* in Redis) rather than adding logging inside your service. A dumped raw value immediately tells you whether the problem is serialization, key naming, or expiry.KeyValueStore interface yourself and rebind it — that's the clean way to add encryption, metrics, prefixing, or locking without forking. If you're outside Tempest's discovery, instantiate the implementation manually and pass it in; don't assume the container exists.remember, no guaranteed atomic increments, and likely no bulk read. If your design needs any of those, budget for building them in the wrapper on day one.How can I help you explore Laravel packages today?