ayvazyan10/imagic
iMagic is a Laravel package for simple, flexible image manipulation. It provides a clean API for resizing, cropping, and caching images, making it easy to generate optimized image variants and integrate dynamic image handling into Laravel applications.
Imagic 2.0 is a Nova image field plus a backing media catalog. The catalog is a real database table, so installation is a two-step affair — the Composer package alone will not work.
composer require ayvazyan10/nova-imagic:^2.0 --with-all-dependencies
php artisan migrate
Then attach the field to a Nova resource like any other image field:
use Ayvazyan10\NovaImagic\Imagic; // confirm exact namespace against the README
Imagic::make('Cover Image')
->disk('s3'); // optional; defaults to your configured disk
Where to look first, in order:
UPGRADE.md — even on a fresh install, this is the fastest way to understand the new storage model (media:<uuid> references, private-by-default visibility, migration of the catalog).SECURITY.md — the package ships authenticated, owner-scoped, rate-limited routes for media delivery; read this so you understand what is already handled for you and what is not.Your first use case should be a single-image field on a resource you already trust, uploaded by a logged-in Nova user. Verify that the stored column contains a media:<uuid> string rather than a URL or path, that the preview renders, and that the row appears in the media library. Once that round-trip works, move to multiple-image fields and S3.
Treat the field value as an opaque reference. After 2.0, the attribute persists media:<uuid>. Do not parse it, do not build URLs from it, and do not assume it is a path. Everything downstream — Nova previews, url() accessors, API responses — should go through the package's resolution so owner-authorization is enforced. This is the single biggest behavioural change from the URL-only approach and the thing most likely to break your existing accessors.
Separate "private" from "public" at the field, not globally. New uploads are private by default, which is the right baseline. When you need a publicly cacheable asset (marketing hero, OG image), override per field rather than flipping the config:
Imagic::make('Hero', 'hero')
->visibility('public');
Keep the config default private and make public an explicit, greppable decision in each resource.
Use multiple-image fields for ordered galleries. The 2.0 multiple field supports keyboard and drag reordering, and the package persists order. That means your front end can rely on the stored sequence instead of a separate order_column you maintain yourself. Pair this with the library's folders so editors self-organize rather than you building an admin CRUD screen for images.
Lean on the media library for reuse. The library is per-user, with nested folders, search, filters, sorting, and pagination. The intended workflow is: an editor uploads once into a folder, then selects the same asset from the library on other resources. Build your content model around that — one canonical asset per concept, referenced from many places — instead of encouraging re-upload per record.
Crop-before-upload belongs in the field config, not in a job. The interactive crop runs client-side before the upload is committed, so there is no "upload then re-process" step to queue. Set your target aspect ratio on the field and let editors crop at entry time; reserve server-side Intervention transformations for derived sizes you generate in bulk.
Handle legacy values with an explicit backfill. URL, path, and JSON values remain readable but are not imported into the catalog. If you are upgrading, keep the old column intact, add the new Imagic field on a separate attribute, and write an artisan command that pulls each legacy file through the upload path so it gets a real media:<uuid> and catalog row. Only then cut over the field and drop the old column. Do not rely on lazy resolution — legacy values never become catalog entries on their own.
Trust the batch rollback, but only within a batch. S3 and S3-compatible writes use random object names with originals and thumbnails, and a failed batch is rolled back. That protects a single multi-file save; it does not protect you from an editor deleting an asset that three other resources reference. Guarded bulk deletion helps, but if you build a "replace image" flow, treat the old media:<uuid> as still-live until you have checked references.
Private by default will surprise you in staging. If previews 403 outside Nova, you almost certainly have a public-facing page trying to read a private asset. The fix is either the ->visibility('public') override or a signed/temporary URL through the package's resolution — not making the bucket public.
Do not disable Composer's security blocking. The release notes are explicit: on EOL framework lines, active advisories have no patched release, so Composer will refuse to resolve. The CI-only override exists to detect Imagic regressions. In production, upgrade Laravel/Nova; do not pass --no-security-blocking or its equivalent. This is the most likely way to cause an incident while "just installing an image field."
Version constraints are narrower than the changelog implies. The security-maintained baseline is PHP 8.2+, Laravel 12, Nova 5, Intervention Image 3.11+. The PHP 8.1+/Laravel 9–11/Nova 4–5/Intervention 2.7 combinations exist strictly as EOL migration paths. Pin ^2.0 and confirm your composer.json platform requirements before you start, or you will burn a day on dependency resolution.
The migration is not optional and not idempotent-by-assumption. php artisan migrate creates the media catalog. In a multi-server deploy, run it as a discrete release step before rolling new app code, and make sure your deploy pipeline does not cache config before the migration has run.
Intervention Image 3 is the real target. 2.7 is a transition path. If you have custom image manipulation code that assumes the 2.x API, it will not survive the move to 3.x — the driver and ImageManager interfaces changed. Isolate that code behind a small service before upgrading.
Debugging media resolution: when a media:<uuid> value fails to render, check in this order — (1) the requesting user is the owner or an authorized viewer, (2) the row still exists in the catalog, (3) the disk in config matches the disk the file was written to, (4) the route is not being blocked by your own middleware or a rate limiter. The package's routes are authenticated and owner-scoped by design, so "it works for me but not for the client" is usually an ownership assumption, not a bug.
Limits are configured, not hardcoded. MIME, extension, byte size, dimensions, pixel count, and batch size are all enforced but all tunable. Set the pixel-count limit deliberately — it is your main defence against decompression bombs — and set batch size to something a phone on mobile data can actually complete, since a failed batch rolls back entirely.
Extension points to prefer: the config file (limits, disk, visibility, folder root), the field's fluent methods (disk, visibility, crop ratio), and a custom disk via Laravel Storage. Avoid subclassing the field or reaching into the media model's internals; the 1.x → 2.x jump reworked storage substantially, and anything touching object naming or paths is the most likely thing to move again.
How can I help you explore Laravel packages today?