Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Key Value Storage Laravel Package

a4sex/key-value-storage

A simple key-value storage package for Laravel/PHP that lets you persist and retrieve arbitrary values through a clean, unified API. Useful for settings, caches, and lightweight configuration without the overhead of a full database model.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and inspect before wiring anything up. This is a small, zero-star package, so budget 20 minutes to read the source before you commit to it:

composer require a4sex/key-value-storage

Then open the package in your editor and answer three questions:

  1. Which class implements Psr\Cache\CacheItemPoolInterface? That is the only object you should ever inject.
  2. What does its constructor need? A PSR-6 pool almost always delegates to a backend (filesystem, array, Redis, PDO). Find the adapter classes under src/ and note which ones ship in the box.
  3. Does it ship a factory or config object? Small packages often have a Storage::create(...) style helper — prefer that over new so upgrades don't break you.

Minimal first use case — cache-aside around an expensive call. Type-hint the PSR interface, never the concrete class:

use Psr\Cache\CacheItemPoolInterface;

final class ExchangeRateProvider
{
    public function __construct(private CacheItemPoolInterface $cache) {}

    public function rateFor(string $currency): float
    {
        $item = $this->cache->getItem('rates.' . strtolower($currency));

        if (!$item->isHit()) {
            $rate = $this->fetchFromApi($currency);   // slow
            $item->set($rate);
            $item->expiresAfter(600);
            $this->cache->save($item);
        }

        return $item->get();
    }
}

Where to look first in the repo: README.md for the intended entry point, composer.json for the PSR-6 (psr/cache) version constraint, and src/ for the concrete pool + any backend adapters. If the README is thin, the test suite is your real documentation — the tests will show the exact construction ceremony and the TTL semantics.

If the package has no DI integration, register it once in your container as a singleton bound to the interface:

$this->app->singleton(CacheItemPoolInterface::class, fn ($app) => /* construct the package pool */);

Implementation Patterns

Always program against Psr\Cache\CacheItemPoolInterface. The whole value of PSR-6 is substitutability: you can swap in a Symfony Cache pool, an array pool for tests, or the package's own pool without touching call sites. Typing against the concrete class throws that away.

Cache-aside helper (the "remember" pattern). Every project reinvents this; write it once and hide the PSR-6 verbosity:

function remember(CacheItemPoolInterface $pool, string $key, int $ttl, callable $compute): mixed
{
    $item = $pool->getItem($key);
    if ($item->isHit()) {
        return $item->get();
    }
    $value = $compute();
    $item->set($value)->expiresAfter($ttl);
    $pool->save($item);
    return $value;
}

Repository wrapper, not raw calls in controllers. Wrap the pool in a domain-specific cache class (RateCache, ProductCache) with methods like getRate(), invalidateRate(). This keeps key construction in one place and stops string literals from spreading across the codebase.

Namespace and version your keys. Prefix with app + domain + version: myapp:v2:rates:usd. That gives you a one-line "flush everything" by bumping the version segment, which is often faster and safer than a full clear() on a production backend.

Bulk writes with deferred saves. When you populate many entries (e.g. warmer scripts, import jobs), batch them:

foreach ($rows as $row) {
    $pool->saveDeferred($pool->getItem('product.' . $row['id'])->set($row));
}
$pool->commit();

This cuts round-trips dramatically on networked backends. Remember: deferred items are not readable via getItem() until commit() runs.

TTL strategy — decide a policy, not a magic number. Use expiresAfter(int $seconds) for relative lifetimes (the common case) and expiresAt(DateTimeInterface) when you need alignment (e.g. expire at midnight UTC). Pass TTLs down from config so ops can tune them without a deploy.

Serialization discipline. PSR-6 requires values to be serializable. Store scalars, arrays, and plain DTOs. If you must store a model, serialise it to an array first — never a closure, a resource handle, or an object holding a PDO connection.

Testing with an in-memory pool. In your test suite, bind CacheItemPoolInterface to the package's array-backed pool (or a symfony/cache ArrayAdapter). Assertions then become trivial and fast, and you never hit the real backend in CI.

Layering with an existing framework cache. If you're in Laravel or Symfony, you already have a cache abstraction. Do not double-wrap. Either use the package as the sole PSR-6 pool and adapt your framework to it, or keep the framework cache for framework concerns and use this pool only in standalone library code. Pick one boundary and document it.

Decorator for observability. Because everything is the PSR-6 interface, you can wrap the package pool with your own decorator that logs hits/misses and times calls, then inject the decorator. This is the single most useful extension point — it tells you whether your cache is actually earning its keep.

Gotchas and Tips

getItem() never returns null and never "fails" on a miss. It returns a CacheItemInterface whose isHit() is false; get() then returns null. Branching on isHit() is correct, branching on hasItem() first is a wasted round-trip. Code that does if ($item = $pool->getItem($k)) is always true and always wrong.

The reserved-key trap. PSR-6 forbids these characters in keys: { } ( ) / \ @ :. rates:usd will throw InvalidArgumentException on a strict implementation. Normalise keys centrally — lowercase, replace illegal chars, or hash the composite parts:

$key = 'rates.' . preg_replace('/[^A-Za-z0-9_.]/', '_', strtolower($currency));

Keys also have a length limit (64 chars in the spec). Long composite keys silently break on some backends — hash them if you're concatenating user input.

TTL edge cases bite. expiresAfter(0) and a null TTL mean different things across implementations — some treat 0 as "expire immediately", others as "never expire". Decide your convention, encode it in your wrapper, and never pass a raw variable TTL straight through. If a value "disappears instantly", this is almost always the cause.

Deferred saves confuse debugging. If getItem() returns a miss right after saveDeferred(), that's expected — the write hasn't happened. Only commit() flushes. And saveDeferred()/commit() return booleans; check them, because a failed commit on a networked backend is otherwise silent.

Silent misses from key drift. The most common production bug with any PSR-6 pool is a key mismatch between the writer and the reader (rates.usd vs rate.usd). Centralising key generation in a wrapper class is the fix; as a diagnostic, log the key on every miss for a day and look for near-duplicates.

Stampede / thundering herd. PSR-6 gives you no locking. When a hot key expires under load, every request recomputes simultaneously. Mitigate with jittered TTLs ($ttl + random_int(0, 30)) or a short-lived mutex in your wrapper around the recompute path. Do this at the wrapper level — the package won't do it for you.

Zero stars means zero safety net. Pin the exact version in composer.json, read the diff before every bump, and check composer.json for an abandoned psr/cache constraint. There is no security policy and no user base to surface bugs, so write a couple of integration tests against the real backend yourself — you're the de facto maintainer of this dependency in your project.

No PSR-16 sugar. PSR-6 is deliberately verbose (item objects, save calls). If your team wants $cache->set('k', $v, 300), that's PSR-16 (psr/simple-cache), a different interface. Either add a thin adapter over this pool or use the full PSR-6 API as designed — don't mix idioms in one codebase.

Extension point to know: the cleanest way to add behaviour (metrics, logging, key hashing, per-tenant prefixing) is a class implementing CacheItemPoolInterface that delegates to the package pool and rewrites keys on the way in and out. That keeps you insulated from the package's internals and makes it trivial to swap the package out later if it goes stale.

Before adopting, verify: which backends actually ship, whether TTL is honoured by the backend (the array adapter often ignores it), and whether clear() is implemented as a full flush or a no-op. Check the tests for those two behaviours specifically — they're where small cache libraries most often cut corners.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core