tjmpromos/sortable-gallery
A Laravel package for adding sortable image galleries to your app. Attach images to Eloquent models, reorder them with drag-and-drop sorting, and render galleries in your views. Streamlines gallery image management and custom ordering.
composer require tjmpromos/sortable-gallery
php artisan vendor:publish --provider="Tjmpromos\SortableGallery\SortableGalleryServiceProvider"
php artisan migrate
Look at three things first in the repo: the config/sortable-gallery.php (storage disk, image conversions, upload rules), the migration that ships with the package (what table/columns it adds and whether it's a polymorphic gallery table or a column on your model), and the src/ directory to confirm the Filament plugin class name before you register it.
Register the plugin on your panel (typically in app/Providers/Filament/AdminPanelProvider.php):
use Tjmpromos\SortableGallery\SortableGalleryPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(SortableGalleryPlugin::make());
}
Your first real use case is almost always a "Product images" or "Project photos" field inside a Filament resource form. Add the gallery component to the resource's form() schema, attach the package trait to the model, and you have upload + drag-to-reorder in the admin without writing a relation manager yourself. From there, render it on the frontend with the package's Blade component (or a small loop over the sorted relation) before you start customizing.
Attach the gallery to an existing model. Add the package's trait/interface to the model (e.g. Product) and, if the package uses a dedicated table, make sure the migration is published and run. Keep a separate gallery relation or column rather than overloading a generic images mutator — the sort order is the whole point, and you want it scoped to one logical gallery.
Use it inside a Filament resource form. Drop the component into form() alongside your normal fields so editors configure the product and its images on the same screen:
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('name')->required(),
SortableGallery::make('images')
->label('Product photos')
->maxFiles(12),
]);
}
Multiple galleries per model. Use the package's gallery/collection name argument (e.g. ->collection('hero') vs ->collection('gallery')) rather than duplicating models. This keeps sorting independent per gallery and lets the frontend request only the set it needs.
Render ordered on the frontend. Always query through the package's ordered scope / relation instead of relying on $model->images returning sorted rows by accident. A typical Blade usage:
@foreach ($product->gallery()->ordered()->get() as $image)
<img src="{{ $image->url('thumb') }}" alt="{{ $image->alt }}">
@endforeach
If the package ships a Blade component, prefer <x-sortable-gallery :model="$product" collection="gallery" /> — it usually handles responsive srcset, lazy loading, and the sort_order join for you.
Reorder persistence. Reordering in the Filament UI writes the new sort_order via a Livewire reorder call. If you expose sorting anywhere else (custom page, API), reuse the package's reorder method rather than writing sort_order directly, so any cache-busting or model events it fires stay consistent.
API / Inertia integration. When images are consumed by an SPA, return id, url, thumb_url, alt, and sort_order from a resource or transformer. Don't send the raw media/attachment records — they leak paths and internal fields you don't want clients depending on.
Tenancy and per-tenant galleries. If you use Filament tenancy, make sure the gallery records are scoped to the tenant (either through the package's owner morph or by adding a tenant_id to the published migration). The default migration is usually global, so check before you deploy.
public in most gallery packages. On S3 you usually need visibility: 'public' or signed URLs; confirm the URL method the Blade component uses before shipping.saved/deleted is the safest hook if the package doesn't already clear your cache.upload_max_filesize, post_max_size, and Livewire's temporary_file_upload.rules/max_upload_time in config/livewire.php.cascadeOnDelete for the owning model. If not, add a deleting model observer to detach and delete gallery rows/files, otherwise orphaned files pile up on S3.php artisan media-library:regenerate if you're on Spatie underneath.--tag if supported, and move the migration timestamp if it must run after your own *_create_* migrations.->visible() or a policy check on the owning model. Without it, any panel user with form access can upload and reorder.getUploadedFileUsing/saveUploadedFileUsing), a custom Blade view for rendering, and model observers for cache/cleanup. Avoid editing package files in vendor/ — copy the view/config out on the first fork-worthy change.composer.json, read the source for the reorder and delete paths, and add a feature test that uploads two images, reorders them, and asserts the frontend order — that one test will catch most regressions when you upgrade.How can I help you explore Laravel packages today?