A lightweight, file-based object store for PHP that persists object graphs by UUID, supports lazy loading, parent auto-updates, and handles deeply nested structures. Includes a simple viewer UI for exploring stored objects.
- Persistent storage by UUID (JSON + metadata)
- Lazy loading of references with transparent replacement in parents
- Safe mode, locking, in-memory caching
- Class stubs for fast listing, class registry
- Automatic class aliasing if a class is unknown at load time
- Simple object storage viewer (view.php)
- Copy the library or require it via Composer in your project.
- Ensure the storage directory is writable.
- PHP 8.0+ recommended.
<?php
use melia\ObjectStorage\ObjectStorage;
$storage = new ObjectStorage(__DIR__ . '/var/object-storage');
// Build a small graph
$child = new ChildObject('child-1');
$parent = new ParentObject('parent-1', $child);
// Store graph (references are auto-managed)
$uuid = $storage->store($parent);
// Load later
$loaded = $storage->load($uuid);
echo $loaded->name; // "parent-1"
echo $loaded->child->name; // Lazy loads "child-1"
- Graphs are serialized deterministically. When an object references another object, the reference is stored as: { "__reference": "" }.
- On load, references become lazy until accessed; then the referenced object is loaded and the parent structure is updated to hold the real object.
- Arrays are traversed recursively; nested objects within arrays are also references and benefit from lazy loading and parent replacement.
- Properties whose declared type allows LazyLoadReference (or object/mixed) are loaded on demand.
- Accessing a property or method on a lazy reference triggers a read from storage.
- After the first access, the placeholder is replaced in the parent object/array so subsequent access is direct.
Tip: Use union types to enable lazy loading where desired, e.g.:
- public LazyLoadReference|ChildObject $child;
If the property type is a concrete class without LazyLoadReference, the loader eagerly resolves and sets the real object.
When a lazy reference loads, it:
- Replaces itself inside its parent (object property or array cell).
- Prevents repeat loads and keeps code simple (you work with the real object thereafter).
- Objects are stored once; all occurrences become references.
- Deeply nested arrays/objects are handled uniformly.
- A simple read-only viewer is included to browse stored objects.
- Open object-storage/view.php in the browser (ensure it can access your storage directory).
- store(object $obj, ?string $uuid = null, ?int $ttl = null): string
- Persists object and its referenced children; returns UUID. Optional lifetime in seconds.
- load(string $uuid, bool $exclusive = false): ?object
- Loads object with locking (shared when $exclusive=false). Returns null if expired.
- exists(string $uuid): bool
- Checks if object data file exists.
- delete(string $uuid, bool $force = false): bool
- Deletes object and its metadata; returns true on success. With $force=true, returns false if not found.
- list(?string $class = null): Traversable
- Iterates UUIDs; optionally filtered by class (via stubs).
- loadMetadata(string $uuid): ?array
- Returns metadata (className, checksum, timestamp, etc.) or null.
- getClassname(string $uuid): ?string
- Returns stored class name for the UUID.
- clearCache(): void
- Clears in-memory caches.
- rebuildStubs(): void
- Rebuilds class stub index.
Use when you need explicit control over concurrent access (e.g., long-running writes, cross-process coordination). ObjectStorage uses the lock adapter internally in store/load/delete; call these for advanced scenarios.
- acquireSharedLock(string $uuid, int $timeout): void
- Obtain a read/shared lock (multiple readers allowed).
- acquireExclusiveLock(string $uuid, int $timeout): void
- Obtain a write/exclusive lock (mutually exclusive).
- releaseLock(string $uuid): void
- Release a held lock (shared or exclusive).
- isLocked(string $uuid): bool
- Check if a lock exists (by any process).
- isLockedByThisProcess(string $uuid): bool
- Check if the current process holds the lock.
- getActiveLocks(): array
- UUIDs currently locked by this process.
When to use:
- Use LockAdapter methods when orchestrating multi-step operations where you must hold a lock across several API calls.
- Rely on internal locking via load/store/delete for single atomic operations.
Use when you need to gate operations globally (e.g., fail-safe after corruption) or query/process-wide state.
- safeModeEnabled(): bool
- Check if safe mode is active.
- enableSafeMode(): bool
- Activate safe mode to prevent further mutations.
- getLifetime(string $uuid): ?int
- Remaining seconds (0 at expiry, negative after expiry, null if unlimited).
- setLifetime(string $uuid, int $ttl): void
- Sets/updates lifetime in seconds.
- expired(string $uuid): bool
- Indicates whether the object is expired (load() returns null for expired objects).
- Locking: shared on load, exclusive on store; cleaned up automatically.
- Caching: in-memory cache avoids repeated deserialization; use clearCache() to reset.
- Safe Mode: if corrupted data is detected, safe mode is enabled to prevent writes until resolved. Disable via disableSafeMode() after fixing.
If a class recorded in metadata does not exist at load time, the storage creates a class alias dynamically so the object can still be instantiated. This keeps old data readable even when types moved or were renamed (ensure you reintroduce real classes or map aliases as part of migrations when possible).
- Prefer union types for properties that should be lazily loaded.
- Keep the storage directory on a fast, reliable disk with proper permissions.
- Use exclusive load/store for read-modify-write sequences.
- Monitor safe mode and logs; rebuild stubs if you reorganize storage.
<?php
$storage = new ObjectStorage(__DIR__.'/var/objects');
$child = new ChildObject('child-X');
$parent = new ParentObject('parent-X', $child);
$uuid = $storage->store($parent);
$p = $storage->load($uuid); // child is lazy
echo $p->child->name; // triggers load and parent replacement
$p->child->name = 'child-X2';
$storage->store($p); // persists changes
<?php
use melia\ObjectStorage\ObjectStorage;
$storage = new ObjectStorage(__DIR__ . '/var/object-storage');
$uuid = '...'; // existing object id
// Read-Modify-Write with exclusive lock + simple retry
$attempts = 0;
$maxAttempts = 3;
do {
try {
// 1) Acquire exclusive lock for this object
$obj = $storage->load($uuid, true); // exclusive
// 2) Modify under lock
$obj->title = 'Updated Title ' . time();
// 3) Persist while still holding the lock
$storage->store($obj);
// 4) Done: lock is released after operations finish
break;
} catch (\Throwable $e) {
$attempts++;
if ($attempts >= $maxAttempts) {
// Could log and rethrow or return a 423 Locked in an API context
throw $e;
}
// Back off briefly before retrying to avoid lock contention
usleep(200_000 * $attempts); // 200ms, 400ms, 600ms
}
} while ($storage->getLockAdapter()->isLockedByOtherProcess($uuid));
AGPL-3.0