CacheLayer is a PHP 8.3+ caching toolkit built around four deliberately separate concerns:
CacheLayer
├── Cache
│ ├── PSR-6 and PSR-16
│ ├── versioned tags
│ ├── bounded stampede protection
│ └── tiering
├── Node Cache
│ └── APCu L1 → SQLite L2
├── Cluster Cache
│ └── durable invalidation between Node Caches
├── Atomic Counters
└── Process-local Memoization
The ordinary cache is disposable storage. Cluster Cache distributes invalidations, not values. Atomic counters remain outside the cache contract because they require stronger semantics. Memoization stays process-local.
composer require infocyph/cachelayerChoose extensions and client packages only for the backends you use: APCu, Redis/Valkey, Memcached, PDO, SysV shared memory, MongoDB, or Cassandra/ScyllaDB.
use Infocyph\CacheLayer\Cache\Cache;
$cache = Cache::sqlite('app', '/var/cache/my-app/cache.sqlite');
$cache->setMultiple([
'profile.1' => ['name' => 'Ada'],
'profile.2' => ['name' => 'Grace'],
], 300);
$profiles = $cache->getMultiple(['profile.1', 'profile.2', 'profile.3']);
$cache->deleteMultiple(['profile.1', 'profile.2']);Cache implements PSR-6, PSR-16, and ArrayAccess. It intentionally does not implement Countable, magic property access, runtime namespace mutation, or compatibility aliases. Keys and tags must be 1–64 characters and match [A-Za-z0-9_.-]+; invalid bulk input is rejected before storage is changed.
A callable passed as the PSR-16 get() default is returned as a value. Use the explicit remember() API to compute and persist a miss:
$user = $cache->remember(
'user.42',
fn () => $repository->find(42),
ttl: 300,
tags: ['users'],
);remember() follows get → miss → lock → recheck → resolve → save → release. Lock waiting is bounded; a timeout computes fail-open and records the unlocked computation. No lock operation occurs on a hit.
$cache->setTagged('article.7', $article, ['articles', 'author.12'], 600);
$cache->invalidateTags(['articles', 'author.12']);Each record embeds its complete tag-version snapshot. Tag versions begin at zero, invalidation increments them atomically, and reads fetch all required versions in a batch. A mismatch makes the complete record stale; there are no per-entry reverse tag indexes or partially tagged writes.
Zero and negative PSR-16 TTLs delete the key. Missing tag metadata means version zero.
Bulk methods validate once and call the adapter’s native bulk contract. Deferred PSR-6 items are also persisted through the same bulk path on commit().
| Backend | Bulk read/write strategy |
|---|---|
| Array memory | direct array lookup/update |
| WeakMap | one prune pass plus direct lookup |
| Null store | immediate misses/no-op writes |
| APCu | array apcu_fetch, grouped stores |
| Redis / Valkey | MGET, MSET, pipelined TTL writes |
| Memcached | getMulti, TTL-grouped setMulti |
| PDO | chunked IN (...), multi-row upsert |
| MongoDB | $in, bulkWrite |
| ScyllaDB | partition-bucketed IN, bounded unlogged batches |
| Shared memory | one lock per batch operation |
| File / PHP files | optimized sequential filesystem access |
| Redis Cluster | fixed hash buckets and same-slot grouped operations |
Redis Cluster uses 128 stable bucket hash tags. Memcached and Redis Cluster clear a namespace by advancing epochs, so they do not scan, flush other namespaces, or maintain a permanent key membership index.
The public factories are:
Cache::memory(); Cache::weakMap(); Cache::nullStore();
Cache::apcu(); Cache::file(); Cache::phpFiles();
Cache::sharedMemory(); Cache::redis(); Cache::valkey();
Cache::redisCluster(); Cache::memcached(); Cache::pdo();
Cache::sqlite(); Cache::mongodb(); Cache::scylla();
Cache::tiered([...]);Data and internal metadata use physically separate key spaces. SQL-like stores can install schema explicitly with PdoCacheSchema::install() and pass initializeSchema: false to PdoCacheAdapter in deployment-controlled environments.
phpFiles creates executable PHP files and is only appropriate for a trusted directory and trusted payloads. Never point SQLite at NFS, SMB, or another shared network filesystem.
$cache = Cache::tiered([
['driver' => 'apcu', 'namespace' => 'app'],
['driver' => 'valkey', 'namespace' => 'app'],
]);A bulk read asks L1 for the full batch, asks later tiers only for remaining keys, and promotes hits upward in batches. Writes and deletes are one batch per participating tier.
Payload and runtime policy is provided at construction and never stored globally:
use Infocyph\CacheLayer\Cache\CacheOptions;
$options = new CacheOptions(
integrityKey: $_ENV['CACHE_INTEGRITY_KEY'],
maxPayloadBytes: 8_388_608,
compressionThreshold: 4096,
compressionLevel: 6,
allowClosures: false,
allowObjects: false,
failOpen: true,
);
$cache = Cache::redis('app', options: $options);Records use only the CacheLayer v2 markers cl2:, cl2-gz:, and cl2-sig:. Compression is threshold-based and retained only when smaller. HMAC verification, payload bounds, bounded decompression, and deserialization policy are isolated per cache instance. Corrupt payloads are safe misses.
Construction and configuration errors throw. Runtime backend failures default to fail-open: reads become misses, writes/deletes return false, and backend_failure is recorded. Set failOpen: false to propagate runtime failures. CacheOptions::fromEnvironment() explicitly reads CACHELAYER_PAYLOAD_INTEGRITY_KEY and CACHELAYER_MAX_PAYLOAD_BYTES; environment state is never read implicitly.
use Infocyph\CacheLayer\Node\NodeCache;
use Infocyph\CacheLayer\Node\NodeCacheConfig;
$node = NodeCache::create(new NodeCacheConfig(
namespace: 'app',
sqliteFile: '/var/cache/my-app/cache.sqlite',
));Node Cache combines an APCu L1 with a local SQLite L2, uses miss-only bulk L2 reads and bulk L1 promotion, and retains WAL, synchronous=NORMAL, bounded busy timeout, bounded pruning, checkpoint, and optimization maintenance.
Cluster Cache adds durable invalidation around independent Node Caches. It keeps per-node cursors, replay, retention-gap recovery, consumer status, key/tag/namespace invalidation, bounded draining, PDO or Redis/Valkey Streams transports, and a transactional outbox.
$runtime->invalidateKey('product.42');
$runtime->invalidateTags(['products', 'catalog']);
$runtime->invalidateNamespace();
$runtime->consume();It does not replicate values and is not a distributed lock, session store, or counter system.
AtomicCounters uses an AtomicCounterStoreInterface; Redis/Valkey is the distributed implementation. Counters are never emulated with cache get() plus set().
The memoize(), remember(object: ...), and once() helpers plus MemoizeTrait provide bounded process-local memoization. They are independent of persistent backend caching.
Metrics distinguish calls from key volume: get_batch, get_batch_keys, hits/misses, set/delete batch counts, tag-version fetches, promotions, lock outcomes, and backend failures. exportMetrics() returns a snapshot and can invoke an export hook.
PHPBench scenarios in benchmarks/ cover single operations, 10/100/1000-key bulk operations, tagged/plain records, tier and Node promotion, codec security/compression, and remember paths. Backend-focused tests separately verify operation counts for native bulk calls. These are microbenchmarks, not production throughput claims.
composer ic:doctor
composer ic:process
composer ic:testsIntegration suites self-skip when their optional service or extension is unavailable.
Do not disclose suspected vulnerabilities in a public issue, discussion or pull request. Follow SECURITY.md and use GitHub private vulnerability reporting.
CacheLayer is protected by PHPForge, which provides automated tests, static and taint analysis, dependency auditing, architecture checks and release-readiness gates. Automated controls do not replace responsible disclosure or manual review.
MIT Licensed
Documentation • Security • Code of Conduct • Contributing
🗂️ Bug • Feature • Documentation • Question • CI failure
🔀 General • Bug fix • Feature • Refactor • Performance • Security & reliability • Documentation • Maintenance