ensalza / ether
Lightweight static PHP cache with file persistence, OPcache acceleration and automatic regeneration.
Requires
- php: >=8.2
- ext-json: *
- brick/varexporter: ^0.7
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^1.11 || ^2.0
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
- ext-opcache: Improves repeated loading of generated PHP cache files
Provides
None
Conflicts
None
Replaces
None
README
Lightweight static PHP cache with file persistence, OPcache acceleration and automatic regeneration.
- PHP
returnfiles as storage, OPcache-friendly reads. - Automatic regeneration through registered generators.
- TTL + manual invalidation, stale-while-revalidate + stale-if-error.
- Strict locking for explicit mutations, recovery-first automatic reads.
- Fully static API, no tags/groups.
Source of truth for behavior: ether-agent-spec/. Work plan: ether-agent-spec/plan.md.
Install
composer require ensalza/ether
Requirements: PHP >=8.2, ext-json, brick/varexporter ^0.7. Suggested: ext-opcache.
Minimal example
use Ensalza\Ether\Ether;
Ether::path('/absolute/path/cache');
Ether::register('productos.catalogo', static fn() => ['a' => 1]);
$data = Ether::get('productos.catalogo');
Register / get
Ether::register('key', $generator, $ttl = 3600);
$value = Ether::get('key');
Registration is declarative: no disk I/O, no path() required. Duplicate normalized key throws. Allowed generators: unbound Closure, global function string, [Class::class, 'publicStaticMethod'], "Class::publicStaticMethod" with zero required params. Bound closures, instance methods, non-public statics and callbacks with required args throw EtherInvalidGeneratorException.
Note: register() accepts mixed $generator (instead of callable) so invalid callbacks raise EtherInvalidGeneratorException rather than TypeError. See ether-agent-spec/plan.md DEC-07.
get() returns runtime snapshot first, then current disk entry without lock, otherwise regenerates under lock with double-check. Valid disk entries read even when unregistered; regeneration requires registration or throws EtherKeyNotRegisteredException. After successful generation, export/write/reload failures are resilient: onError() notified, generated value returned and runtime-cached.
Set
Ether::set('key', $value, $ttl = null);
Works registered or not. Strict + blocking lock before inspecting state. TTL: explicit > registered > global default. Always fresh generation, invalidated_at reset. Returns reconstructed persisted value (identity may differ) and runtime-caches it. Reload failure deletes entry and throws EtherReadException.
Invalidate vs delete
Ether::invalidate('key'); // soft: keeps files for stale, marks invalidated_at
Ether::delete('key'); // hard: removes php+meta, keeps .lock, idempotent
Both strict + lock-before-check + clear runtime. invalidate() preserves original timestamp if already invalidated, cleans orphan meta, no rewrite when meta missing/corrupt. Use delete() when stale must be impossible.
Stale behavior
Servable as stale (coherent generation, plausible PHP header): TTL-expired, explicitly invalidated, missing/corrupt metadata. Never stale: format_version != 1, generation-ID mismatch, empty/corrupt PHP, bad header, key mismatch. No max stale age, no cooldown in v0.1.0.
- Lock contention: stale returned immediately, not runtime-cached.
- Generator failure + usable stale:
onError(GENERATION)+ stale, not runtime-cached. refresh()never uses stale fallback.
Concurrency overview
set/refresh/invalidate/delete: blockingLOCK_EXbefore state check, serialize correctly.get()current: lock-free. Stale:LOCK_EX|LOCK_NB, contention → stale. Miss: blocking lock + double-check. Never regenerates without lock; lock-mechanism failure + stale →onError(LOCK)+ stale, elseEtherLockException.clear(): maintenance-only, not concurrency-safe, no per-key locks. Use only in controlled maintenance.
OPcache note
After PHP replace, opcache_invalidate($file, true) best-effort. Multi-host NFS requires working cross-node flock(), atomic rename() visible across nodes and opcache.validate_timestamps=1. Local invalidate affects only current host; others rely on timestamp validation.
Cache path warning
- Absolute paths only, outside document root, controlled by app user.
- Setup creates dir (umask respected, no chmod), resolves realpath, checks writable + probe write/delete.
- Trusted application-controlled storage: generated PHP files are included. Never make cache dir writable by untrusted users. See
SECURITY.mdanddocs/storage-format.md.
Persistent workers / reset
Runtime cache is a per-process request snapshot: once current value loaded, same process returns it even if disk expires/invalidates elsewhere. Local set/refresh replace runtime, invalidate/delete clear it, clear/reset empty it. Stale-from-contention is never runtime-cached so later get() can observe fresh data.
In long-lived workers, call Ether::reset() between jobs to clear path/registrations/runtime/defaultTTL/handler (memory only, never disk), then reconfigure. reset() is advanced API, mainly for tests/workers. clear() preserves registrations but empties runtime.
Docs
docs/api.md,docs/errors.md,docs/concurrency.md,docs/storage-format.mdexamples/are executable (require __DIR__ . '/../vendor/autoload.php')