php-extended/php-simple-cache-filesystem
A filesystem-backed PSR-16 simple cache implementation for PHP. Store and retrieve cache items as files with TTL support, offering a lightweight, dependency-free way to add persistent caching to Laravel or any PHP project.
composer require php-extended/php-simple-cache-filesystem. It pulls in psr/simple-cache and the sibling php-extended/php-simple-cache-interface package, so you get the PSR-16 CacheInterface plus the library's own extended interfaces.src/ in the package (and the php-simple-cache-interface dependency). The concrete class implements Psr\SimpleCache\CacheInterface; confirm its constructor signature there — php-extended packages typically take a directory path (sometimes wrapped in an options/configuration object) and may accept a default TTL.storage_path('app/cache/psr16')), instantiate the cache once, and exercise the PSR-16 surface:use Psr\SimpleCache\CacheInterface;
$cache = app(CacheInterface::class); // bound in a service provider, see below
$key = 'report_42_v1';
if (! $cache->has($key)) {
$cache->set($key, $payload, 3600); // 1 hour
}
$payload = $cache->get($key, $default = null);
set, get, delete, clear against a temp directory in a throwaway script before wiring anything into the app. The filesystem layout it produces (one file per key, or sharded subdirectories) tells you a lot about the implementation.Bind the interface, not the class. Register a singleton in AppServiceProvider and resolve Psr\SimpleCache\CacheInterface everywhere. That keeps consumers portable to any other PSR-16 backend later (Redis, array, etc.) without code changes.
$this->app->singleton(\Psr\SimpleCache\CacheInterface::class, function ($app) {
$dir = storage_path('app/cache/psr16');
if (! is_dir($dir)) {
mkdir($dir, 0775, true);
}
return new \PhpExtended\SimpleCache\SimpleCacheFilesystem($dir);
});
Cache-aside is the default workflow. get() with a default, compute on miss, set() with an explicit TTL. Because this is plain PSR-16 (no tags, no atomic increment), don't try to model counters or invalidation groups — design keys so they can be deleted individually or cleared wholesale.
Namespace your keys per feature and per version. Prefix like billing:v2:invoice_123. PSR-16 reserves { } ( ) / \ @ : in keys, so the : separator used above is actually illegal — use _ or . instead (billing.v2.invoice_123). Versioning keys is the cheapest way to invalidate after a schema change.
Batch where it matters. getMultiple() / setMultiple() / deleteMultiple() are part of PSR-16; use them for warming a page's worth of fragments rather than N round-trips, and pass a per-item TTL map to setMultiple() when different fragments age differently.
Integrate with Laravel without fighting it. This is not an Illuminate\Contracts\Cache\Store, so don't expect Cache::tags() or Cache::remember(). Two clean options:
Illuminate\Contracts\Cache\Store that delegates to the PSR-16 object, then Cache::extend() a psr16 driver. Only worth it if you want Laravel's remember/put ergonomics across the whole app.Isolate per environment / per app. Point each app (or each test run) at its own subdirectory. clear() on a shared directory is a blunt instrument; separate roots make teardown safe.
Testing. In tests, bind the interface to a temporary directory under storage/framework/testing/ created in setUp() and removed in tearDown(). For unit tests where filesystem behavior is irrelevant, swap in symfony/cache's ArrayAdapter-backed PSR-16 implementation or any in-memory PSR-16 class — the interface binding makes this a one-line change.
Decorator pattern for extension. Because everything is behind PSR-16, you can wrap the filesystem cache in a decorator that adds in-memory memoization for hot keys, logging, or metrics, and still satisfy the same interface.
InvalidArgumentException for keys containing { } ( ) / \ @ : or the empty string. Laravel developers habitually write user:1:profile — that will throw. Sanitize or hash keys at the boundary.null means "no expiry"; an int is seconds; a DateInterval is also accepted. Zero or negative TTLs are implementation-defined (often treated as "delete now" or "already expired") — check the source rather than assuming.www-data; a directory created by a deploy script as another user will fail on write or on clear(). Create the directory with the runtime user, or chmod/chown in your deploy step. clear() needs delete permission on subdirectories, not just files.rename(); if not, simultaneous writers to the same key can produce a partially written file and an unserialize error. For hot keys, either accept the rare miss or add a lock.clear() on a namespaced subdir) if the package doesn't ship one.clear() scope. Confirm whether it wipes the whole root directory or only files it recognizes. Point it at a dedicated directory so it can't delete unrelated storage.open_basedir restrictions, NFS (rename semantics vary), and Windows path-length limits. Local disk is the intended target.get() always misses, check: directory writability, whether keys are being normalized/hashed differently on read vs write, TTL actually being applied, and clock/timezone drift on the host.file cache store or symfony/cache's FilesystemAdapter are better-supported drop-in alternatives with the same PSR-16 contract.How can I help you explore Laravel packages today?