Search by

ensalza / ether

ufito3000

Lightweight static PHP cache with file persistence, OPcache acceleration and automatic regeneration.

v0.1.1 2026-10-02 09:24 UTC

This package is auto-updated.

Last update: 2026-10-02 09:25:33 UTC


README

Lightweight static PHP cache with file persistence, OPcache acceleration and automatic regeneration.

  • PHP return files 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: blocking LOCK_EX before 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, else EtherLockException.
  • 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.md and docs/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.md
  • examples/ are executable (require __DIR__ . '/../vendor/autoload.php')