fsi/gallery-bundle
FSi Gallery Bundle provides Doctrine-backed image galleries for Symfony applications, including photo uploads, thumbnail generation, and Twig helpers for rendering albums. Drop it into an existing admin to manage galleries and media with minimal setup.
Reality check first: fsi/gallery-bundle is a Symfony2 bundle, not a Laravel package. It ships no ServiceProvider, no facade, no config publish tag, and its last release was 2015-03-10 (Symfony 2.x / PHP 5.x era). You cannot composer require it into a Laravel app and expect php artisan to find it. What you can do is use it as a reference implementation and port it, or run it behind a Doctrine-based bridge (see Gotchas). Plan accordingly before you sink a sprint into it.
If you are evaluating it in a Symfony context (or porting it), the minimum path is:
README.md first — it's short and shows the intended flow: create a Gallery, attach Photos, attach the gallery to any entity implementing GalleryableInterface.DependencyInjection/Configuration.php to see the actual config tree (storage paths, image sizes/thumbs, entity class overrides). This tells you more than the docs do.Entity/Gallery.php, Entity/Photo.php, and Entity/GalleryHasPhoto.php — that pivot entity is the heart of the model: it carries position for ordering photos inside a gallery.Resources/config/doctrine/*.orm.xml for the exact schema before writing any migration.Resources/views/ for the Twig partials and Twig/ for the rendering extension.First use case to target: "An Article has zero-or-many Galleries; each Gallery has ordered Photos; the front end renders a thumbnail grid with a lightbox." That's the one flow this bundle was built for. Get that working before touching anything else.
For a Laravel translation of that first use case, the equivalent skeleton is:
// migrations: galleries, photos, gallery_photo (pivot with position + galleryable morphs)
Schema::create('gallery_photo', function (Blueprint $t) {
$t->id();
$t->foreignId('gallery_id')->constrained()->cascadeOnDelete();
$t->foreignId('photo_id')->constrained()->cascadeOnDelete();
$t->unsignedInteger('position')->default(0);
$t->timestamps();
});
The bundle's model is a three-table graph: Gallery ⟷ Photo through GalleryHasPhoto (a pivot with payload — position). In Laravel this maps almost 1:1:
class Gallery extends Model {
public function photos(): BelongsToMany {
return $this->belongsToMany(Photo::class)
->withPivot('position')
->orderBy('gallery_photo.position')
->withTimestamps();
}
}
The orderBy on the pivot column is the pattern to preserve — it's the whole reason a non-trivial pivot entity exists.
The bundle's GalleryableInterface lets any domain object declare getGalleries() so galleries can be attached to articles, products, pages, etc. The Laravel analogue is a polymorphic many-to-many:
// on Gallery
public function galleryable() { return $this->morphTo(); }
// or, if galleries own the pivot:
public function attachTo(Model $model, int $position = 0) {
$model->galleries()->attach($this->id, ['position' => $position]);
}
If you need galleries shared across multiple owners (the bundle supports this), model it as galleryables (morph pivot) rather than a galleryable_id column on galleries — otherwise you'll be rewriting migrations in month two.
The bundle delegates file handling to fsi/resource-bundle and Symfony's filesystem, storing a relative path on Photo. That's a local-disk assumption baked in. In Laravel, replace it wholesale:
Storage::disk('public') (or S3) and store the disk-relative key, never an absolute path.spatie/laravel-image-optimizer), and persist dimensions + variants as JSON on the photos row. The bundle's config-driven "image sizes" section becomes a config/gallery.php array of variants — keep it a config file so front-end code never hardcodes sizes.mime, width, height, alt, caption as columns; the bundle's Photo entity has title/description fields that are easy to forget and painful to backfill.Twig helpers (gallery_* functions) and partials are the presentation contract. For Laravel, port each Twig partial to a Blade component (<x-gallery.grid :gallery="$gallery" />, <x-gallery.lightbox />) so views stay swappable. Don't inline rendering logic into controllers — that's the one habit that makes a later migration away from this package expensive.
The bundle was designed to drop into fsi/admin-bundle. There is no Laravel equivalent, so choose deliberately: Filament (closest ergonomics), Nova, or a hand-rolled Livewire form. Whatever you pick, keep the ordering UI explicit — a sortable list with a position integer is enough, and it mirrors GalleryHasPhoto exactly.
Mirror the bundle's config tree into config/gallery.php (variants, disk, thumbnail driver, max upload size, allowed mime types). Bind entity class names via config only if you actually plan to override them; otherwise hardcode and move on.
composer require on a current Laravel app will either fail dependency resolution or drag in dead Symfony 2 packages. Treat this as a reference codebase, not a dependency.GalleryHasPhoto.position is the most-missed detail. If you collapse it to a plain belongsToMany without withPivot('position'), ordering silently falls back to insertion order and breaks the moment someone reorders.Resources/views/* as the acceptance criteria for your Blade components; screenshot-diff against it.doctrine/orm under Laravel via laravel-doctrine/orm and boot this bundle's mappings, but you'll be maintaining two persistence layers for one feature. Only consider it if the rest of the app is already Doctrine-based.spatie/laravel-medialibrary for the file/variant layer and a thin gallery_photo pivot for ordering. You'll get 90% of this bundle's feature surface with an actively maintained dependency, and you'll keep the one genuinely reusable idea — an ordered pivot plus a polymorphic "galleryable" attachment — without inheriting a 2015 Symfony bundle.How can I help you explore Laravel packages today?