biigle/laravel-file-cache
A Laravel package for caching file contents and related data with automatic invalidation when the underlying file changes. It provides a simple file-aware cache layer to speed up repeated reads while keeping cached results fresh.
The package solves one narrow problem well: given some source of a file (a path on a local disk, an object on an S3-style disk, or an HTTP(S) URL), give me a durable local copy plus a database record I can reference later. It is not a general asset pipeline, so the fastest way to evaluate it is to wire it to a single real use case — e.g. "download a user-supplied PDF/image from a URL once, then serve it from disk forever."
Minimal setup:
composer require biigle/laravel-file-cache
php artisan vendor:publish --provider="Biigle\FileCache\FileCacheServiceProvider"
php artisan migrate
Then inspect, in this order:
config/file-cache.php — the disk the cache writes to, retention/max-age, and any route/middleware settings. This is the file you will actually change.FileCache model — this tells you what is stored per file (source path or URL, local path, size, MIME type, timestamp). Everything else in your app should reference this record, not the raw URL.Biigle\FileCache\FileCache service class — the entry point you inject or resolve.First use case, in a controller or job:
use Biigle\FileCache\FileCache;
$cache = app(FileCache::class);
// Remote URL, local path, or "disk:path" — same API, different source
$file = $cache->get('https://example.org/reports/q1.pdf');
// $file is the FileCache record; the package gives you the local path
// (and, if route serving is enabled, a URL) from it.
return response()->file($file->path);
If you only have ten minutes, verify two things: (a) calling get() twice on the same source does not re-download, and (b) php artisan migrate created the table and the file physically appears on the configured disk. If both hold, the rest of the package is detail. Note that exact method and attribute names (get(), path, url, etc.) have shifted across releases — check the README for your pinned version before copying snippets.
Resolve once, store the ID. The single most valuable habit: after fetching, persist file_cache_id on your own model (or a polymorphic relation) instead of re-passing the URL everywhere. Your Report row then points at a cache record that can be re-served, re-pruned, and inspected independently of the origin. It also makes deletion deterministic — when the owning model is deleted, you know exactly which cache entry to release.
On-demand fetch in a request path, but only for small/slow-tolerant files. The fetch is synchronous. For thumbnails, avatars, or small documents this is fine and keeps the code tiny:
public function avatar(User $user)
{
$file = app(FileCache::class)->get($user->avatar_url);
return response()->file($file->path, ['Content-Type' => $file->mime_type]);
}
For anything user-facing and large, push it into a queued job and have the request path only ever hit already-cached entries (404 or placeholder if missing).
Warming and pre-fetching. A scheduled command that iterates your sources and calls get() is the natural companion to the synchronous read path. Combine it with the package's pruning/cleanup command in the same schedule so fetch and eviction stay in balance. This is the pattern the package is really built for: fetch out-of-band, serve cheaply in-band.
Serving through the package vs. serving yourself. If the service provider registers a file-serving route, use it when you need centralised auth middleware, signed URLs, or a stable public URL that outlives your storage layout. Serve yourself (response()->file(), StreamedResponse) when you need range requests, custom headers, or you are already behind your own authorization layer. Do not do both for the same disk — pick one and make it the only public path.
Force refresh and invalidation. Treat the cache as derived data. When the origin changes, or when you knowingly pushed a bad file, call the service in a way that bypasses/replaces the existing entry (force flag or delete-then-get). Wire an admin action or an event listener so operators are not editing the table by hand.
Testing integration. Storage::fake() the cache disk and Http::fake() the remote origin; then assert on both the database row and the fake disk contents. For expiry/retention behaviour, freeze time (travel() / Carbon::setTestNow()) rather than waiting on real timestamps.
->delete() the row ad hoc.app/Console/Kernel.php (or the scheduler) on day one, otherwise the disk grows silently until someone notices in production.response()->file() or hand paths to other PHP code. If you point the config at an S3 disk, you can still cache, but serving has to stream through the app (Storage::disk(...)->response(...)), which changes your performance profile entirely.Cache::lock("file-cache:{$sourceKey}") and re-check after acquiring; on a busy endpoint this is the difference between one download and fifty.Content-Type and size after the fetch before you trust the record, and set an explicit HTTP timeout on the underlying client — the default may be longer than your request timeout.APP_URL and TrustProxies must be right or every signature fails intermittently. This is a classic "works locally, 403s in staging" bug.get() if you expect hits.bootstrap/app.php, copy the config file by hand (vendor:publish is unavailable), and either load the migration from the package path or copy it into database/migrations. Facades and route registration may also need explicit enabling.composer.json and re-run your integration test on upgrade, particularly around expiry semantics and the served-file URL.How can I help you explore Laravel packages today?