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 Store Laravel Package

webmozart/key-value-store

Webmozart KeyValueStore offers a simple PHP abstraction for key-value storage with pluggable backends, letting applications switch between local and remote stores without changing code. Useful for caching, configuration, and persistent maps.

Deep Wiki
Context7

Getting Started

Install and first look

composer require webmozart/key-value-store

Before writing any code, read Webmozart\KeyValueStore\Api\KeyValueStore. The entire contract is six methods:

interface KeyValueStore
{
    public function exists($key);
    public function get($key);              // throws NoSuchKeyException
    public function set($key, $value);
    public function setIfNotExists($key, $value); // returns bool
    public function remove($key);
    public function removeAll();
}

Everything else in the package is a backend implementation of that interface (ArrayStore, FilesystemStore, JsonFileStore, RedisStore, MemcachedStore, DoctrineCacheStore, RiakStore, NullStore).

Minimal first use case: a persistent runtime flag

use Webmozart\KeyValueStore\Api\NoSuchKeyException;
use Webmozart\KeyValueStore\ArrayStore;

$store = new ArrayStore();
$store->set('billing.enabled', true);

try {
    $enabled = $store->get('billing.enabled');
} catch (NoSuchKeyException $e) {
    $enabled = false;
}

Wire it into Laravel once

In a service provider, bind the interface rather than the concrete class so tests and environments can swap backends:

// app/Providers/KeyValueServiceProvider.php
use Webmozart\KeyValueStore\Api\KeyValueStore;
use Webmozart\KeyValueStore\ArrayStore;
use Webmozart\KeyValueStore\RedisStore;
use Predis\Client;

public function register(): void
{
    $this->app->singleton(KeyValueStore::class, function ($app) {
        if ($app->environment('testing')) {
            return new ArrayStore();
        }

        return new RedisStore(new Client([
            'host' => config('database.redis.default.host'),
            'port' => config('database.redis.default.port'),
            'database' => 3, // do not share with cache/queues
        ]));
    });
}

Inject KeyValueStore into the few services that need durable key-value semantics. That is the whole onboarding: one interface, one binding, one try/catch around get().

Implementation Patterns

Contract-first injection, backend chosen at the edge. Never new RedisStore(...) in domain code. Type-hint KeyValueStore; let the provider decide whether that is ArrayStore, JsonFileStore, or RedisStore. This is the package's main selling point and the only reason to prefer it over Cache:: calls scattered through the codebase.

Environment matrix. A pattern that works well with this package:

Environment Store Why
testing ArrayStore Fast, isolated, no external services
local JsonFileStore Inspectable file, no Redis required
production RedisStore Shared across app servers

Because the interface has no TTL or connection semantics, the swap is genuinely transparent — unlike swapping Laravel cache drivers where tags() or increment() may disappear.

Atomic "first writer wins" for locks and idempotency. setIfNotExists() maps to SETNX/add in the backing stores. This is the day-to-day feature Laravel's Cache::add() also gives you, but here it is explicit in the contract:

$acquired = $store->setIfNotExists("lock:report:{$id}", getmypid());

if (! $acquired) {
    return; // another worker owns it
}

try {
    $this->generateReport($id);
} finally {
    $store->remove("lock:report:{$id}");
}

Treat it as a best-effort mutex, not a distributed lock with expiry — see gotchas.

Manual TTL envelope. Since the API has no expiration, store an envelope and check it yourself:

$store->set('otp:'.$userId, [
    'value' => $code,
    'expires_at' => now()->addMinutes(10)->getTimestamp(),
]);

$record = $store->get('otp:'.$userId); // wrap in try/catch
if ($record['expires_at'] < time()) {
    $store->remove('otp:'.$userId);
    // treat as expired
}

Only do this for low-volume data. If you find yourself reimplementing expiry, use Laravel's cache instead.

Namespacing by hand. Most backends are flat. Prefix keys at the repository layer:

final class FeatureFlags
{
    public function __construct(private KeyValueStore $store) {}

    public function enabled(string $flag): bool
    {
        try {
            return (bool) $this->store->get("app:flags:{$flag}");
        } catch (NoSuchKeyException) {
            return false;
        }
    }
}

A single FeatureFlags/SettingsRepository wrapper keeps key formatting, exception handling, and serialization assumptions in one place — and makes it trivial to migrate to Cache::rememberForever() later.

Store snapshots and typed payloads deliberately. Values are serialized by the backend (see gotchas), so arrays and small DTOs round-trip. This is useful for "last processed cursor" or "previous sync payload" records. Keep payloads small and version them (['v' => 2, ...]) because changing a DTO's properties will break unserialize() on old data.

Testing pattern. Bind ArrayStore in the container and assert against it directly; use NullStore when you want to prove code paths tolerate a store that never has anything. Seed ArrayStore with its constructor array where supported:

$this->app->instance(KeyValueStore::class, new ArrayStore([
    'app:flags:new-checkout' => true,
]));

Integrate at seams, not everywhere. Good fits: feature flags, small settings, cursors/watermarks, idempotency markers, dedupe keys for webhooks. Poor fits: session data, cache of expensive queries, queues, anything needing counters or scans. Those belong to Laravel's cache/Redis abstractions.

Gotchas and Tips

Missing keys throw, they don't return null. get() raises NoSuchKeyException. Every read is a try/catch or an exists() check — and exists() followed by get() is a race. Prefer catching the exception. Wrap reads in a repository method so the try/catch doesn't leak into controllers.

No TTL, no increment, no scan/keys. The interface is intentionally minimal. If you need expiry, counters, tags, or key listing, this package is the wrong tool; use Illuminate\Support\Facades\Cache or Redis directly. The envelope pattern above is a workaround, not a feature.

Not PSR-16 (psr/simple-cache). You cannot drop it in wherever a CacheInterface is expected, and Laravel's cache repository does not implement this interface. Expect to write an adapter in either direction.

Values are serialize()d by most backends. Consequences:

  • Objects require the class to be autoloadable and unchanged on read; refactors break stored data.
  • Closures and resources fail.
  • Raw Redis/Memcached inspection shows opaque serialized strings — expected, not corruption.
  • Data is not portable to other languages or to a JSON-based cache. If interop matters, store JSON strings and json_decode yourself.

Key normalization is a real source of collisions. Keys are converted to strings; 1 and "1" are the same key. Long or unusual keys may be rejected or behave differently per backend. Namespace with a stable string prefix and avoid user-supplied keys without sanitizing.

Redis backend uses its own client (Predis), not Laravel's connection. You get a second connection pool, separate auth/DB settings, and none of Laravel's key prefixing. Pick a dedicated database number (e.g. database => 3), or accept silent key collisions with cache/session/queue keys. Also check cluster/Sentinel compatibility of whatever Predis version you install.

setIfNotExists() is atomic on Redis/Memcached but is not a safe distributed lock. There is no expiry, so a crashed worker leaves a permanent lock. Add a timestamp in the value and clean up stale locks in a scheduled command, or use Cache::lock() instead.

FilesystemStore / JsonFileStore caveats. Watch directory permissions, concurrent writers, and container read-only filesystems. removeAll() on a filesystem store deletes everything under its directory — never point it at a shared path. Atomic rename behavior matters if multiple PHP-FPM workers write.

Memcached key limits. 250-byte keys, no spaces/control characters. Validate or hash long keys before they reach the store, or you'll get silent misses/misses-as-errors depending on the client.

Package age is the biggest day-to-day risk. Last release was 2016. Expect friction on PHP 8.1+ (deprecations in dependencies, Serializable deprecations), and Composer conflicts with modern Laravel/Symfony versions, often via the pinned webmozart/assert. Practical options, in order:

  1. Confirm it installs at all in your app; if not, don't force it.
  2. Fork the handful of classes you use (the API is tiny) into app/Support, or
  3. Replace it with a thin KeyValueStore interface of your own backed by Cache/Redis — you keep the swappable-backend design without the 2016 dependency graph.

Extension points. Implementing KeyValueStore yourself is trivial (six methods) if you need a database-backed or S3-backed store. Decorators (prefixing, logging, read-through caching) are easy to add around the interface — often more valuable than adding another backend.

Debugging checklist.

  • Missing key? Log $key plus the resolved prefix before catching.
  • Serialized garbage in Redis? Confirm you're reading the right DB and that the value was written by this package.
  • Tests behaving differently? Verify ArrayStore is actually bound — a leaked singleton from a previous test is a common cause.
  • Swapped backend and behavior changed? Check setIfNotExists return semantics and key validation rules per implementation.
  • If you must inspect production, use redis-cli --scan --pattern 'your:prefix:*' and unserialize() locally rather than guessing from raw bytes.
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.
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
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect