outerweb/image-library
A Laravel package for building and managing image libraries in your application. It provides a simple, consistent API for storing, organizing, and retrieving images, keeping image handling centralized and reusable across your project. Ideal for apps that need organized image management.
The package is small and unopinionated, so the fastest path to productivity is to let its published config and migration tell you what it actually does rather than reading the README twice.
composer require outerweb/image-library
php artisan vendor:publish --tag="image-library-config"
php artisan vendor:publish --tag="image-library-migrations"
php artisan migrate
Read config/image-library.php first. In a package this size, the config file is the documentation — it will show you the storage disk, the base path/URL prefix, the registered conversion sizes, and any route/middleware settings. Everything else in the package keys off those values.
Make sure the disk is actually reachable:
php artisan storage:link
and confirm FILESYSTEM_DISK / the package's configured disk points at something with a public URL, otherwise rendered images will 404 while uploads succeed.
use Outerweb\ImageLibrary\Traits\HasImages;
class Post extends Model
{
use HasImages;
}
<x-image-library::image :image="$post->images->first()" conversion="card" />
Get that single happy path working end-to-end (upload → stored file → rendered <img>) before touching conversions, because almost every problem you'll hit later is a storage-path or URL problem, not a package-API problem.
Treat it as a thin layer over the filesystem, not a CMS. The package's value is the glue: model attachment, path conventions, conversion definitions, and a render helper. Keep your own domain logic (who can upload, what gets attached to what) in your application code and use the package only for storage + rendering.
Centralize upload handling in a form request + action. Don't scatter addImage() calls through controllers:
class StorePostRequest extends FormRequest
{
public function rules(): array
{
return ['images.*' => ['image', 'max:5120']];
}
}
// in the controller
foreach ($request->file('images', []) as $file) {
$post->addImage($file);
}
Validate image and a max size before the package touches anything — the package will happily store a 40MB PNG and your conversions will then time out.
Define conversions once, in config, named by intent. Resist per-call ad-hoc sizes. thumb, card, hero in config means templates stay readable and you can regenerate everything later from one place. If a conversion is only used in one view, it's probably a sign the design hasn't settled.
Use it in seeders and factories. For local dev and tests, generating a real file beats mocking:
$post->addImage(UploadedFile::fake()->image('hero.jpg', 1600, 900));
This is the single highest-leverage pattern for day-to-day work — it makes image-dependent views testable and makes migrate:fresh --seed produce a realistic site instead of a wall of broken thumbnails.
Render through the component, not raw paths. If you need srcset, loading="lazy", or width/height attributes for CLS, wrap the package's URL helper in your own Blade component rather than spreading <img> tags across templates. That gives you one place to fix alt text handling and one place to swap the package out later.
Output JSON through an accessor, not the model's raw attributes. For an API, add an image_url / image_urls accessor or API Resource so the storage path never leaks into your public contract. If you later change disks or move to a CDN, only the resource changes.
Deletion: hook it into the model lifecycle explicitly. Whatever the package does or doesn't do on deleting, verify it. The safe pattern is a booted() hook on the model or an observer that removes stored files, plus a scheduled image-library:prune-style command (or your own) for orphans left by failed uploads.
max_execution_time. Move generation to a queued job either way.php artisan config:cache in production means edits to conversion sizes silently do nothing until you re-cache. Any "my new size isn't rendering" bug is this, nine times out of ten.Storage::disk(config('image-library.disk'))->exists($path) before blaming the component. A missing storage:link, a disk without 'url' => env('APP_URL').'/storage', or an APP_URL mismatch behind a proxy will produce 404s that look like package bugs.Storage::fake() in tests. Real-disk tests are slow and leave junk. Assert on Storage::disk(...)->assertExists(...) for the path, and separately assert the rendered HTML contains the expected URL — those are two different failure modes and blurring them makes debugging harder.alt column alongside the image record (or on the pivot, if images are shared) and make it required in your form request. Retrofitting accessibility is much more expensive than including it now.public/ directly. Let the package write to the configured disk with its own path convention; hand-rolled paths defeat the point and will collide with cleanup logic.resources/views/vendor/image-library/), the config-driven conversion list, and any bound interfaces in the service provider. For a 12-star package, expect the maintainer's own needs to define the API — if you need substantially different behaviour, wrapping it in your own ImageService is cheaper than fighting it.^1.0 constraint and a quick look at the changelog on update will save you a broken deploy.How can I help you explore Laravel packages today?