melia / object-storage
File-based object store for PHP: whole object graphs by UUID in plain files, lazy loading, crash-safe writes, locking, unique and search indexes, an SQL-like query language (OQL, experimental), a CLI and a viewer.
Requires
- php: ^8.5
- ext-json: *
- psr/simple-cache: ^3.0
- symfony/cache: ^8.0
- symfony/console: ^8.0
- symfony/lock: ^8.0
Requires (Dev)
- ext-xdebug: *
- fakerphp/faker: ^1.24
- loupe/loupe: ^1.1
- monolog/monolog: ^3.0
- perftools/php-profiler: ^1.4
- phpstan/phpstan: ^2.3
- phpunit/phpunit: ^13.0
- twig/twig: ^3.30
Suggests
- ext-apcu: For caching operations
- ext-memcached: For caching operations
- ext-redis: For caching operations
- loupe/loupe: For the Loupe search engine (melia\ObjectStorage\Search\Loupe\LoupeEngine); needs ext-intl and ext-pdo_sqlite
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-09 21:40:42 UTC
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 server when you run large ad-hoc queries over many objects: unique and search indexes speed up lookups, and OQL (experimental) answers ad-hoc questions with joins and aggregates. Queries that no index serves 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. |
| Indexes | Unique indexes (like a UNIQUE constraint) and search indexes with full text, filters, facets and joins (Loupe included). |
| OQL (experimental) | An SQL-like query language with joins, paths into referenced objects, GROUP BY and INSERT/UPDATE/DELETE, in code, the CLI and the viewer. |
| 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 |
| OQL | Querying objects with SQL: SELECT with joins, paths into referenced objects, GROUP BY, aggregates, ORDER BY, dates and NOW(), INSERT/UPDATE/DELETE, ALTER … DROP; in code, the CLI and the viewer. Learn it with OQL by example |
| 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 |
| Transactions | Storing and deleting several objects together, rolled back on failure and after crashes |
| 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
- OQL by example — the query language step by step: conditions, paths, joins, lists, grouping, dates and
NOW(), writing, adding and dropping properties; every query tested - Renaming classes — keeping stored data loadable after a refactoring
Versioning and stability
ObjectStorage follows Semantic Versioning from 1.0 on:
- Covered by the backward compatibility promise: public classes, interfaces and methods, the protected methods of
ObjectStorageand of classes meant to be extended (e.g.Strategy\Standard), the CLI commands and their options, and the storage format on disk. A breaking change of these only comes with a new major version. - Not covered: everything marked
@internal(e.g. the OQL parser, planner and executor,Query\Ast), the web viewer, and the files undertests/andprofiling/. These may change in any release. - Experimental: OQL (
melia\ObjectStorage\Query\Oqland the classes it returns, the CLI commandquery, the query console of the viewer) is marked@experimental. Its syntax, semantics and API may still change in a minor release; such changes are listed in the Changelog under Breaking (experimental). - Interfaces you implement (
StrategyInterface,WriterInterface,File\IO\AdapterInterface,LockAdapterInterface, search engine interfaces) may only get new methods in a major version. - Storage format: data written by a 1.x version can be read by every later 1.x version. Where a release changes the format, older data is still read or a migration is shipped (see Upgrading). This includes the data of unique and search indexes and of maintenance tasks, which is stored as objects in internal storages: the class names and properties of these records (e.g.
Unique\Claim,Search\Index\Definition,Util\UuidTrie,Util\Maintenance\TaskState) are part of the format, even where the classes are@internal. Search indexes can always be rebuilt from the stored objects.
Changelog and license
Changes are documented in the Changelog, upgrade steps in UPGRADE.md. The library is licensed under the Apache License 2.0.