Search by

christianjbrown / key-value-store

christianjbrown

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

Statistics

Installs: 352

Dependents: 4

Suggesters: 0

Stars: 0

Open Issues: 0

v3.0.1 2026-10-01 11:29 UTC

README

CI Coverage Packagist License PHP

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 an expiresAt field.
  • 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-firestore is not a hard requirement of this library (it pulls in ext-grpc), so it is only suggested — install it yourself if you use this store: composer require google/cloud-firestore (and enable ext-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) becomes new FirestoreKeyValueStore($document, $clock).
  • new FirestoreKeyValueStoreFactory($adapterFactory) becomes new FirestoreKeyValueStoreFactory($adapterFactory, $clock). Use new Symfony\Component\Clock\NativeClock() in production.
  • new MemoryKeyValueStore() becomes new MemoryKeyValueStore($clock), and the TTL is now enforced.
  • A custom SecretManagerClientInterface implements accessLatest(string $versionName): ?string and addVersion(string $secretName, ?string $value): void instead of taking and returning Google request and response objects. Throw SecretManagerClientException when the service fails.
  • GoogleSecretKeyValueStore, GoogleSecretKeyValueStoreFactory and DefaultSecretManagerClientFactory keep 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 a GoogleSecretKeyValueStoreFactoryInterface.
  • FirestoreKeyValueStore::create($client, $collection, $id) is gone. Use (new FirestoreKeyValueStoreFactory(new DefaultFirestoreDocumentAdapterFactory()))->create($client, $collection, $id), or inject a FirestoreKeyValueStoreFactoryInterface.
  • FirestoreKeyValueStore's constructor now takes a FirestoreDocumentAdapterInterface instead of a Google\Cloud\Firestore\DocumentReference. Wrap a reference with new FirestoreDocumentAdapter($reference).
  • FirestoreDocumentReferenceFactoryInterface and DefaultFirestoreDocumentReferenceFactory are replaced by FirestoreDocumentAdapterFactoryInterface and DefaultFirestoreDocumentAdapterFactory, which return an adapter rather than a DocumentReference.
  • create() is removed from GoogleSecretKeyValueStoreInterface and FirestoreKeyValueStoreInterface.

📝 Changelog

Notable changes in each release are listed in CHANGELOG.md.

📄 License

Released under the MIT License.