melia / object-storage
Lightweight PHP file-based object store with UUID persistence, lazy-loading references, in-memory cache, file locking, safe mode, and a simple viewer UI.
Requires
- php: >=8.5
- ext-json: *
- psr/simple-cache: ^3.0
- symfony/cache: ^6.0
- symfony/console: ^6.0
- symfony/lock: ^6.4
Requires (Dev)
- ext-xdebug: *
- fakerphp/faker: ^1.24
- monolog/monolog: ^2.10
- perftools/php-profiler: ^1.4
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^10
- sebastian/object-graph: ^3.0
- twig/twig: ^3.19
Suggests
- ext-apcu: For caching operations
- ext-memcached: For caching operations
- ext-redis: For caching operations
Provides
None
Conflicts
None
Replaces
None
README
ObjectStorage
Store PHP objects as they are — whole object graphs, by UUID, in plain files. No database, no schema, no mapping.
$uuid = $storage->store($order); // the order, its customer and all its lines $order = $storage->load($uuid); // related objects are loaded lazily when you touch them echo $order->customer->name;
ObjectStorage is a file-based object store for PHP. It persists your objects including everything they reference, loads related objects only when they are accessed, and keeps the data safe against crashes and concurrent access.
When to use it
- Prototypes and small to medium applications that want to persist domain objects without designing tables first.
- Caches and snapshots of computed or fetched data, with a lifetime per object.
- Tools, CLI applications and background jobs that need durable state without running a database server.
- Document-like data (configurations, drafts, workflow state) that is mostly loaded and stored as a whole.
It is not a replacement for a database when you need indexes, joins or ad-hoc queries over large amounts of data: queries are PHP callbacks that scan the stored objects of a class.
Features
| Object graphs | Stores an object and everything it references; every object gets its own UUID, shared and circular references are kept. |
| Lazy loading | Referenced objects are loaded on first access and then replace their placeholder transparently. |
| Querying | List and count objects per class, filter them with PHP callbacks, refine results step by step. |
| Lifetime | Optional time-to-live per object, e.g. for caches. |
| Concurrency | Shared and exclusive locks per object, based on symfony/lock (files, Redis, Memcached, databases, …). |
| Durability | Crash-safe writes with checksums; interrupted writes are recovered, corrupted data puts the storage into a read-only safe mode. |
| Caching | In-memory cache by default, any PSR-16 cache (APCu, Redis, …) can be plugged in. |
| Events | Hooks for every operation, e.g. for audit logs, indexes or metrics. |
| Refactoring support | Renamed classes can be mapped, unknown classes still load. |
| Tooling | A CLI for inspection and maintenance, and a small web viewer for development. |
Requirements
- PHP 8.5 or newer with
ext-json - Symfony 8 (
symfony/console,symfony/cache,symfony/lock), installed with the package - A writable directory for the storage
Installation
composer require melia/object-storage
Quick start
<?php use melia\ObjectStorage\ObjectStorage; use melia\ObjectStorage\UUID\AwareInterface; use melia\ObjectStorage\UUID\AwareTrait; require __DIR__ . '/vendor/autoload.php'; class Customer implements AwareInterface { use AwareTrait; // lets the storage write the UUID back into the object public function __construct(public string $name) {} } class Order implements AwareInterface { use AwareTrait; /** @var string[] */ public array $items = []; public function __construct(public mixed $customer) {} } $storage = new ObjectStorage(__DIR__ . '/var/storage'); // store: the order and the customer are stored as two objects $order = new Order(new Customer('Ada')); $order->items = ['keyboard', 'mouse']; $uuid = $storage->store($order); // load: the customer is loaded on first access $order = $storage->load($uuid); echo $order->customer->name; // Ada // update and delete $order->items[] = 'monitor'; $storage->store($order); $storage->delete($uuid);
Continue with Getting started for the details of storing, loading and identifying objects.
Documentation
| Topic | What you will find |
|---|---|
| Getting started | Creating a storage, storing, loading, updating and deleting objects, UUIDs |
| Concepts | Object graphs, lazy loading, caching and what is stored on disk |
| Querying | Listing, counting and filtering objects |
| Unique indexes | Values no two objects of a class may share, like a UNIQUE constraint; finding objects by them |
| Search indexes | Full-text search, filters and facets with exchangeable engines (Loupe included), managed in the viewer |
| Lifetime | Expiring objects, e.g. for caches |
| Copying and moving | Copying and moving objects between storages, in code, the CLI and the viewer |
| Concurrency | Locks, read-modify-write, lock backends, multiple processes |
| Durability and safe mode | Crash-safe writes, checksums, recovery and safe mode |
| Configuration | Constructor options, caches, strategy and persistence policies |
| Events | Reacting to storage operations |
| Serialization | Which objects can be stored, hooks, renamed classes |
| Maintenance and tools | CLI, viewer, rebuilding indexes, migrations |
| Internals | For contributors: how loading and storing work, the naming of the methods |
Examples
Complete scenarios, each runnable as is:
- A blog with lazily loaded comments — object graphs, lazy loading, querying
- Caching API responses — lifetime, a fast setup without fsync, Redis locks
- Inventory with concurrent updates — exclusive loads, read-modify-write
- An audit log from events — events with the previous and the new state
- Product search — a search index with full text, filters, sorting and facets
- Coming from Doctrine ORM — a shop with entities, repositories, cascades, unique keys, queries and full-text search, compared with Doctrine
- Renaming classes — keeping stored data loadable after a refactoring
Changelog and license
Changes are documented in the Changelog. The library is licensed under the Apache License 2.0.