folklore/image
Folklore Image adds flexible image handling to Laravel Eloquent models. Attach uploads to any model, crop and resize them, and generate multiple configured sizes automatically, with a simple trait-based API and Intervention Image powering the transformations.
folklore/image is a thin Laravel wrapper around Imagine that turns image resizing into a URL-generation problem: you ask the facade for a URL with dimensions/options baked in, the package lazily generates and caches the derivative, and serves it through its own route. That is the whole mental model — once you have it, the API is small.
Install. The package is archived and last tagged in 2015, so Composer will likely refuse a normal require on modern PHP. Pin the tag you actually intend to run (composer require folklore/image:^0.x) and expect to test on the PHP version it supports; if you're on PHP 8 you will probably need a fork or to vendor it manually. Treat this as a "greenfield only if you must keep parity with a legacy app" dependency.
Register provider and facade in config/app.php:
'providers' => [
Folklore\Image\ImageServiceProvider::class,
],
'aliases' => [
'Image' => Folklore\Image\Facades\Image::class,
],
Publish the config (php artisan vendor:publish --provider="Folklore\Image\ImageServiceProvider") and read it top to bottom — this file is the documentation. The keys you care about immediately:
driver — gd, imagick, or gmagickcache_path — the writable directory where derivatives landcache_prefix / route name — the URL segment the package serves fromtemplates — named, reusable size presetsMake the cache directory writable and confirm the route the provider registers isn't shadowed by your own catch-all.
First use case — a 300×200 cropped thumbnail in a Blade view:
<img src="{{ Image::url('/uploads/photo.jpg', 300, 200, ['crop' => true]) }}" alt="">
Hit the page. If the image renders, the route, cache path, and driver are all correct. If you get a 404 from your app, the package route is being intercepted; if you get a 500, it's almost always the cache directory not being writable.
Templates over ad-hoc dimensions. The single highest-value thing you can do is centralise sizes in the config file rather than scattering [100, 100] through templates:
'templates' => [
'thumb' => ['width' => 150, 'height' => 150, 'crop' => true, 'quality' => 80],
'hero' => ['width' => 1600, 'height' => 700, 'crop' => true, 'quality' => 85],
'preview' => ['width' => 400, 'height' => null],
],
Then reference them by name (Image::url($path, 'thumb')). This keeps the design system in one place and makes it trivial to re-render everything later — the template name becomes part of the cache key, so bumping 'thumb' to 160 invalidates old files automatically.
Blade usage for responsive sets. Because url() only produces a string, you can loop over breakpoints:
@foreach ([480, 768, 1200] as $w)
<source srcset="{{ Image::url($post->image, $w) }}" media="(min-width: {{ $w }}px)">
@endforeach
Keep the option array identical across the loop so the cache key logic stays predictable.
Model accessors. Wrap the facade in a model method so views never know about storage paths or option arrays:
public function thumbUrl(int $w = 150, int $h = 150): string
{
return Image::url($this->image_path, $w, $h, ['crop' => true]);
}
This is also the seam where you swap to a CDN later: change the accessor, not fifty templates.
Pre-warming the cache. Lazy generation means the first visitor to hit a cold URL pays the cost (and can time out on large source files). If the package version you have exposes a cache/console command (image:cache in some releases), wire it into your deploy script so thumbnails exist before traffic arrives. If it doesn't, write an Artisan command that loops over your models' image paths and calls Image::url() for each template — hitting the URL endpoint populates the cache as a side effect. A warm deploy is the difference between a fast launch and a 504 spike.
Programmatic manipulation. For anything the URL syntax can't express (saving an upload at a specific size, compositing), drop to the Imagine object: Image::open($path) returns the underlying image, giving you resize(), crop(), save() etc. Use this for upload pipelines; use url() for display.
URL as cache key — lean into it. Every distinct combination of path, dimensions, and options is a separate cached file. That is the package's core strength and its main operational cost. Design your templates so the number of combinations stays bounded (e.g. fixed breakpoints, not arbitrary user-supplied widths).
It is archived. Plan for the exit. No security patches, no PHP 8 fixes, no Laravel 9+ compatibility. Pin the exact version in composer.lock, and keep the model accessor pattern above so a future migration to intervention/image is a one-file change.
The cache key is the URL, not the source file. Overwrite photo.jpg on disk and every cached derivative stays stale — the package has no mtime check in the typical setup. Either version your filenames (photo-{hash}.jpg) or purge the derivative directory on deploy. This bites people constantly.
Query strings are part of the URL. Derivatives are addressed with query parameters. Some CDNs and reverse proxies (and many mod_rewrite rules) ignore or strip query strings when computing cache keys — verify your CDN is configured to vary on the full query string, or you'll serve a 150px thumb where a 1200px hero belongs.
Path handling is the security boundary. Image::url() takes a path and the route reads from disk. Confirm in your version that the resolved path is constrained to configured roots; if not, wrap all inputs in a validator that rejects .. and absolute paths. Never pass a raw user-supplied path straight through.
Driver differences are real. GD is slower and weaker on transparency, colour profiles, and animated GIFs; Imagick handles EXIF orientation and CMYK far better but can be denied by shared hosts. Set driver explicitly per environment and don't rely on the default.
EXIF rotation isn't automatic. Phone uploads will render sideways unless you explicitly rotate (via the Imagine object or an orientation/rotate option). Handle it at upload time, not at display time.
Memory limits, not just speed. Resizing a 6000px source with GD loads the whole thing into memory. The classic symptom is a fatal Allowed memory size exhausted on the first request for a large image while small ones work fine. Add a max source dimension check at upload.
Config changes need php artisan config:clear. In a cached-config production setup, editing templates or cache_path appears to do nothing until you clear it — and if you change cache_path, old derivatives are orphaned in the previous directory.
Cleaning up. Expiry-based cleanup (if configured) runs opportunistically; there's no background garbage collector. Long-running sites accumulate thousands of orphans. Schedule a periodic sweep of files older than N days in the cache directory, and remember every template resize multiplies file count by the number of source images.
url() returns a relative path in most configurations. Behind a CDN or subdirectory deploys, wrap it (asset() / url()) at the accessor layer rather than in every template.
Don't put the image route behind auth middleware accidentally via a broad web group. Public thumbnails will 302 to login, and <img> tags silently show broken icons instead of surfacing the error.
How can I help you explore Laravel packages today?