christianjbrown / key-value-store
A thin, strongly-typed PHP 8.5+ library of interchangeable key-value store implementations (database, Google Secret Manager, Google Firestore, in-memory) behind a single mockable interface.
Package info
github.com/christianjbrown/key-value-store-php
pkg:composer/christianjbrown/key-value-store
Requires
- php: ^8.5
- ext-pdo_mysql: *
- doctrine/dbal: ^4.4.3
- doctrine/orm: ^3.6.7
- google/cloud-secret-manager: ^2.0
- psr/clock: ^1.0
Requires (Dev)
- christianjbrown/code-quality-scripts: ^1.0
- google/cloud-firestore: ^1.55 || ^2.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^13.0
- symfony/clock: ^7.0||^8.0
Suggests
- google/cloud-firestore: Required only for FirestoreKeyValueStore (pulls in ext-grpc).
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-01 13:35:39 UTC
README
A small, strongly-typed PHP library of interchangeable key-value stores. Every store hides
behind one tiny contract — KeyValueStoreInterface — so you can read, write, and update a single
?string value (with an optional ?int TTL) without caring where it actually lives. It's built for
small pieces of state such as configuration flags, cursors, or OAuth refresh tokens.
Four implementations ship today:
- Database (
DatabaseKeyValueStore) — persists via Doctrine ORM to any database it supports (MySQL/MariaDB, PostgreSQL, SQLite, SQL Server, …), keyed by a string id. - Google Secret Manager (
GoogleSecretKeyValueStore) — reads/writes a Secret Manager secret. - Google Firestore (
FirestoreKeyValueStore) — reads/writes a single Firestore document; serverless and connectionless, with TTL support via anexpiresAtfield. - In-memory (
MemoryKeyValueStore) — a per-process value, handy for tests and defaults.
Because they share one interface, calling code can accept a KeyValueStoreInterface and stay
oblivious to the backing store.
✔️ Prerequisites
💡 If you're on MacOS and have Homebrew, PHP and Composer will install with brew install composer.
🏗️ Installation
For your composer-enabled project:
composer require christianjbrown/key-value-store
💻 Usage
Every store exposes the same three methods:
public function getTtl(): ?int; public function getValue(): ?string; public function setValue(?string $value, ?int $ttl = null): self;
💾 Database key-value store
Persists each key to a row in a database table through Doctrine ORM — so it works with any platform Doctrine DBAL supports (MySQL/MariaDB, PostgreSQL, SQLite, SQL Server, …). First, define a concrete entity by extending the provided mapped superclass and giving it a table:
use ChristianBrown\KeyValueStore\AbstractDatabaseKeyValueStoreEntity; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] #[ORM\Table(name: 'key_value_store')] class KeyValueStoreEntity extends AbstractDatabaseKeyValueStoreEntity { }
Then construct a store with your Doctrine EntityManager, the entity class name, and the key:
use ChristianBrown\KeyValueStore\DatabaseKeyValueStore; $store = new DatabaseKeyValueStore($entityManager, KeyValueStoreEntity::class, 'refresh-token'); $store->setValue('a-secret-token', 3600); // value + optional TTL (seconds) $value = $store->getValue(); // 'a-secret-token', or null if the key has never been set $ttl = $store->getTtl(); // 3600, or null
Passing an entity class that does not extend AbstractDatabaseKeyValueStoreEntity (i.e. does not
implement DatabaseKeyValueStoreEntityInterface) throws InvalidArgumentException.
🔒 Google Secret Manager key-value store
Reads and writes a Google Secret Manager secret. Build one
with GoogleSecretKeyValueStoreFactory, which asks a SecretManagerClientFactoryInterface for a client.
DefaultSecretManagerClientFactory builds the real client from the GOOGLE_APPLICATION_CREDENTIALS
environment variable:
use ChristianBrown\KeyValueStore\DefaultSecretManagerClientFactory; use ChristianBrown\KeyValueStore\GoogleSecretKeyValueStoreExceptionInterface; use ChristianBrown\KeyValueStore\GoogleSecretKeyValueStoreFactory; $factory = new GoogleSecretKeyValueStoreFactory(new DefaultSecretManagerClientFactory()); $store = $factory->create('projects/my-project/secrets/my-secret'); try { $value = $store->getValue(); // the latest secret version's value, or null $store->setValue('new-value'); // adds a new secret version } catch (GoogleSecretKeyValueStoreExceptionInterface $e) { // the secret could not be read or written print $e->getMessage(); }
In application code, type-hint GoogleSecretKeyValueStoreFactoryInterface and inject the factory. To
use your own client, implement SecretManagerClientInterface (accessLatest() and addVersion(), throwing
SecretManagerClientExceptionInterface on failure), or wrap a
Google\Cloud\SecretManager\V1\Client\SecretManagerServiceClient in GoogleSecretManagerClientAdapter,
and pass it straight to the constructor:
$store = new GoogleSecretKeyValueStore($client, 'projects/my-project/secrets/my-secret');
⚠️ Secret Manager has no notion of a TTL, so this store implements only KeyValueStoreInterface
(getValue() and setValue()), with no getTtl() and no $ttl argument.
🔥 Google Firestore key-value store
Reads and writes a single Google Firestore document. It is
serverless and connectionless — no VPC connector or database connection to manage. The value and an
integer expiresAt unix timestamp are stored as two fields on the document. FirestoreKeyValueStoreFactory
builds the store from a FirestoreClient, a collection name, and a document id:
Optional dependency.
google/cloud-firestoreis not a hard requirement of this library (it pulls inext-grpc), so it is only suggested — install it yourself if you use this store:composer require google/cloud-firestore(and enableext-grpc). The other stores are unaffected.
use ChristianBrown\KeyValueStore\DefaultFirestoreDocumentAdapterFactory; use ChristianBrown\KeyValueStore\FirestoreKeyValueStoreFactory; use Google\Cloud\Firestore\FirestoreClient; use Symfony\Component\Clock\NativeClock; $firestoreClient = new FirestoreClient(); $factory = new FirestoreKeyValueStoreFactory(new DefaultFirestoreDocumentAdapterFactory(), new NativeClock()); $store = $factory->create($firestoreClient, 'kv', 'my-key'); $store->setValue('a-secret-token', 3600); // value + optional TTL (seconds) $value = $store->getValue(); // 'a-secret-token', or null if unset or expired $ttl = $store->getTtl(); // remaining seconds, or null when no TTL was set
getValue() returns null when the document does not exist or its expiresAt has passed; getTtl()
returns the remaining seconds (expiresAt minus the clock's current time), or null when no expiry is set. In application code,
type-hint FirestoreKeyValueStoreFactoryInterface and inject the factory. The store itself only talks
to a FirestoreDocumentAdapterInterface (getFields() and setFields()), so you can also pass your
own implementation, or a FirestoreDocumentAdapter around a Google\Cloud\Firestore\DocumentReference,
straight to the constructor:
$store = new FirestoreKeyValueStore(new FirestoreDocumentAdapter($documentReference), $clock);
Both the store and the factory take a PSR-20 Psr\Clock\ClockInterface, so the current time is read
from the clock on every call and tests can pass a fixed one.
⚡ In-memory key-value store
A per-process value that lives only for the current request. It takes a PSR-20 clock and honours the
TTL like the other TTL-aware stores: the value reads as null once the TTL has passed, and getTtl()
reports the seconds left.
use ChristianBrown\KeyValueStore\MemoryKeyValueStore; use Symfony\Component\Clock\NativeClock; $store = new MemoryKeyValueStore(new NativeClock()); $store->setValue('hello', 60); $store->getValue(); // 'hello', or null after 60 seconds $store->getTtl(); // 60, counting down
🚨 Error handling
GoogleSecretKeyValueStore normalizes Secret Manager access failures into a single library exception
that implements ChristianBrown\KeyValueStore\GoogleSecretKeyValueStoreExceptionInterface (which
extends Throwable), so one catch covers both read and write failures:
use ChristianBrown\KeyValueStore\GoogleSecretKeyValueStoreExceptionInterface; try { $value = $store->getValue(); } catch (GoogleSecretKeyValueStoreExceptionInterface $e) { print $e->getMessage(); }
The database store throws InvalidArgumentException if constructed with an entity class that does not
implement DatabaseKeyValueStoreEntityInterface.
⬆️ Upgrading to 3.0
Version 3.0 injects a PSR-20 clock and narrows the Secret Manager client interface.
new FirestoreKeyValueStore($document)becomesnew FirestoreKeyValueStore($document, $clock).new FirestoreKeyValueStoreFactory($adapterFactory)becomesnew FirestoreKeyValueStoreFactory($adapterFactory, $clock). Usenew Symfony\Component\Clock\NativeClock()in production.new MemoryKeyValueStore()becomesnew MemoryKeyValueStore($clock), and the TTL is now enforced.- A custom
SecretManagerClientInterfaceimplementsaccessLatest(string $versionName): ?stringandaddVersion(string $secretName, ?string $value): voidinstead of taking and returning Google request and response objects. ThrowSecretManagerClientExceptionwhen the service fails. GoogleSecretKeyValueStore,GoogleSecretKeyValueStoreFactoryandDefaultSecretManagerClientFactorykeep their signatures.
⬆️ Upgrading to 2.0
Version 2.0 removes the static create() factories and puts Firestore behind an adapter. Nothing else
changes.
GoogleSecretKeyValueStore::create($path)is gone. Use(new GoogleSecretKeyValueStoreFactory(new DefaultSecretManagerClientFactory()))->create($path), or inject aGoogleSecretKeyValueStoreFactoryInterface.FirestoreKeyValueStore::create($client, $collection, $id)is gone. Use(new FirestoreKeyValueStoreFactory(new DefaultFirestoreDocumentAdapterFactory()))->create($client, $collection, $id), or inject aFirestoreKeyValueStoreFactoryInterface.FirestoreKeyValueStore's constructor now takes aFirestoreDocumentAdapterInterfaceinstead of aGoogle\Cloud\Firestore\DocumentReference. Wrap a reference withnew FirestoreDocumentAdapter($reference).FirestoreDocumentReferenceFactoryInterfaceandDefaultFirestoreDocumentReferenceFactoryare replaced byFirestoreDocumentAdapterFactoryInterfaceandDefaultFirestoreDocumentAdapterFactory, which return an adapter rather than aDocumentReference.create()is removed fromGoogleSecretKeyValueStoreInterfaceandFirestoreKeyValueStoreInterface.
📝 Changelog
Notable changes in each release are listed in CHANGELOG.md.
📄 License
Released under the MIT License.