Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Gallery Json Media Laravel Package

webplusm/gallery-json-media

A Filament 4 JSON media field for images and documents, with a fluent API and Blade components for Laravel front-ends. Manage galleries, add custom properties, and render photos or document links easily.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and match your Filament version. The package tracks Filament majors on separate branches, and this is the single biggest source of setup failure:

Filament Package Branch
v3.x 3.x 3.x
v4.x 4.x main / 4.x
v5.x 5.x 5.x
composer require webplusm/gallery-json-media

Filament v4+ requires PHP 8.2. You also need spatie/image capabilities working, which means a real image driver (GD or Imagick) available in PHP.

2. Publish config and wire the theme. Two steps that people skip and then file bugs about:

php artisan vendor:publish --tag="gallery-json-media-config"
php artisan vendor:publish --tag="gallery-json-media-translations"

Then, in your panel's theme.css (create a custom theme first if you don't have one — Filament v4 styling docs):

@import '../../../../vendor/webplusm/gallery-json-media/resources/css/json-media.css';

Without this import the gallery renders as unstyled boxes and drag/drop looks broken. Don't publish views unless you're prepared to maintain them across upgrades.

3. Prepare the model. You need both the cast and the interface/trait pair:

use GalleryJsonMedia\JsonMedia\Concerns\InteractWithMedia;
use GalleryJsonMedia\JsonMedia\Contracts\HasMedia;

class Page extends Model implements HasMedia
{
    use InteractWithMedia;

    protected $casts = [
        'images'    => 'array',
        'documents' => 'array',
    ];

    protected function getFieldsToDeleteMedia(): array
    {
        return ['images', 'documents'];
    }
}

The getFieldsToDeleteMedia() method is what makes orphan cleanup happen when the record is deleted. Omit it and your disk fills with orphaned crops.

4. First use case to build. A Page with a images JSON column and a documents JSON column, edited in a Filament resource, displayed on a Blade front end. That exercises the whole package: form field → storage → JSON → getMedias() → custom Blade output. Confirm php artisan storage:link has been run before you start debugging "images don't show".

Where to look first: the 3.x branch README if you're on Filament v3 (the root README documents v4/v5), the config/gallery-json-media.php for the crop driver, and the Media class in src/JsonMedia/Media.php — that's the object you'll be touching daily on the front end.

Implementation Patterns

The "one JSON column per media kind" pattern. Don't try to put images and documents in one column. Two columns, two fields, two entries. It keeps image() vs document() semantics clean and keeps your custom-property schemas from colliding.

Form field configuration. Build a private method on the resource so the field definition is reused across create/edit without drift:

JsonMediaGallery::make('images')
    ->directory('page')
    ->image()
    ->reorderable()
    ->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp'])
    ->maxSize(4 * 1024)
    ->minFiles(1)
    ->maxFiles(10)
    ->downloadable()
    ->deletable()
    ->replaceTitleByAlt()
    ->withCustomProperties(
        customPropertiesSchema: [
            TextInput::make('alt')->label('Alt text'),
            TextInput::make('credit')->label('Credit'),
        ],
        editCustomPropertiesOnSlideOver: true,
        editCustomPropertiesTitle: 'Edit media details',
    )
    ->editableCustomProperties(fn () => auth()->user()->can('edit-media'))

Notes on that block:

  • image() or document() — pick exactly one. Not both, not neither.
  • replaceTitleByAlt() is the pragmatic default for editorial sites: the filename stops leaking into the UI and editors set alt text where they'd expect to.
  • editableCustomProperties() accepts a Closure, which is the cleanest way to gate metadata editing by policy rather than by hiding the field entirely.
  • withCustomProperties() schema fields become the whitelist for $media->getCustomProperty('key'). Anything not in the schema won't be surfaced in the UI, so treat the schema as your data contract.

Grid list display. In the resource table, the README shows JsonMediaGallery::make('images')->displayOnGrid() — that's a typo; the grid toggle belongs on the column, not the field:

use GalleryJsonMedia\Tables\Columns\JsonMediaColumn;

JsonMediaColumn::make('images')
    ->avatars()          // bool or Closure for compact rendering
    ->displayOnGrid();

Use avatars() on tables you scan — user lists, article indexes. Use the plain column when the thumbnail itself is the point.

Infolists for read-only screens. Two distinct components, and they are not interchangeable:

JsonMediaEntry::make('images')
    ->avatars()
    ->thumbHeight(100)
    ->thumbWidth(100)
    ->visible(static fn (?array $state) => filled($state));

JsonDocumentsEntry::make('documents')   // gives you download links
    ->columns(4)
    ->columnSpanFull();

The ->visible(fn (?array $state) => filled($state)) guard is worth standardizing on. Empty JSON columns produce empty grids, and empty grids look like bugs in a review screenshot.

Front-end: default loop vs. controlled rendering. Start with the default:

@foreach ($page->getMedias('images') as $media)
    {{ $media }}
@endforeach

Media implements __toString() and returns an <img> (or a link for documents). That's fine for prototypes. The moment you need layout control, switch to an explicit view and stop relying on __toString():

@foreach ($page->getMedias('images') as $media)
    {!! $media->withImageProperties(width: 200, height: 180)
              ->withView('page.json-media') !!}
@endforeach
{{-- resources/views/page/json-media.blade.php --}}
@php
    use GalleryJsonMedia\JsonMedia\Media;
    /** @var Media $media */
@endphp
<figure style="width: {{ $media->width }}px">
    <img loading="lazy"
         src="{{ $media->getCropUrl(width: $media->width, height: $media->height) }}"
         alt="{{ $media->getCustomProperty('alt') }}"
         width="{{ $media->width }}"
         height="{{ $media->height }}">
</figure>

For documents, always use the object API rather than string casting — you want the label and the URL separately:

@foreach ($page->getDocuments('documents') as $document)
    <a href="{{ $document->getUrl() }}" target="_blank" rel="noopener">
        {{ $document->getCustomProperty('title') ?? 'Download' }}
    </a>
@endforeach

Backfilling after an upgrade. If you've been on the package since before v5.5, images uploaded previously have no recorded dimensions, so srcset and thumbnails are wrong (thumbnails could upscale small images). Declare your models and run the regenerate command:

// config/gallery-json-media.php
'maintenance' => [
    'models' => [
        App\Models\Page::class => ['images', 'documents'],
    ],
],
php artisan gallery-json-media:regenerate --dimensions-only
php artisan gallery-json-media:regenerate --thumbnails-only

Run the two passes separately on a large library so a failure in dimension reading doesn't leave you unsure which thumbnails were re-rendered. This command writes without firing model events or touching updated_at, which is exactly what you want in production.

Testing. composer test runs the package suite; in your app, use Storage::fake() and assert against the JSON column contents rather than asserting on rendered HTML. The media array shape is the contract you actually depend on.

Gotchas and Tips

Public disk only — no S3. ->visibility() is documented as public-only. If your architecture assumes S3/R2 for media, this package is the wrong tool and you'll discover it late. Verify this against config/gallery-json-media.php (filesystem config) before committing; the crop URLs are generated as plain public paths, not signed/temporary URLs.

Crop driver must exist. The config exposes:

'images' => ['driver' => ImageDriver::Gd], // or ImageDriver::Imagick

Gd is the safe default but memory-hungry on large JPEGs; Imagick is faster and handles exif rotation consistently but must be compiled in. Pick one deliberately and pin it per environment — a local Gd / production Imagick split will produce visibly different crops between staging and prod.

Thumbnails are generated at upload time. That means your web request carries the crop cost. Bulk imports and multi-file drags on modest hosting will time out or exhaust memory. If your editors upload 20 photos at once, raise max_execution_time/memory_limit for the panel route or batch the uploads.

JSON columns are not queryable in useful ways. You cannot meaningfully where('images->0->name', ...) across your app, and you shouldn't try. If you need "find all pages using this image" or "media library" behaviour, this package is a per-record gallery, not a media library — plan a separate index table if that's a real requirement.

Custom properties are per-file, not global. Each media entry stores its own properties. Renaming a schema key (alt → alt_text) does not migrate existing JSON; you'll need a one-off data migration over the JSON column. Treat property keys as frozen once content editors have used them, and add new keys instead of renaming.

Orphan cleanup depends on your trait method. getFieldsToDeleteMedia() handles deletes through the model. Files removed from the gallery without saving (a known v4.0.7 fix area) and records deleted via raw queries will still leave files behind. Schedule a periodic reconciliation job that diffs disk contents against your JSON columns — this is the single most valuable operational habit with this package.

Reorder state and first-load behaviour. Reorderable galleries had a first-load rawState bug fixed in v4.0.7. If you're below that, drag order can corrupt on first open. Pin at least ^4.0.7 (or the equivalent 3.x/5.x patch) and test reorder-then-save explicitly — it's the interaction most likely to silently lose data.

Theme CSS drift on upgrade. Because you import the package stylesheet from vendor/, a composer update can change class names and break your panel styling without any change to your own code. After any package upgrade, open one gallery form and one infolist and eyeball them before deploying.

Version-lock the whole trio. Filament major, package major, and PHP version move together. The changelog shows rapid branch churn (v2.x → v4.0.0 → v5.0 → v5.5). Constrain with "webplusm/gallery-json-media": "^4.1" style pins and read CHANGELOG.md before any composer update touching Filament.

Bus factor and lock-in. The package is a single-maintainer project with no dependents, so treat it as a dependency you may one day have to fork or replace. Two mitigations that cost almost nothing: (1) always access media through your own wrapper methods on the model — getGalleryImages(), getGalleryDocuments() — rather than calling getMedias() directly in thirty Blade files; (2) keep the JSON shape documented in the model so a future migration to a proper media table is a mechanical mapping, not a reverse-engineering exercise.

Extension point worth knowing. The custom Blade view hook (withView()) plus withImageProperties() is the sanctioned escape hatch. Anything the built-in __toString() output can't do — <picture> elements, lazy-loaded lightboxes, per-media CSS classes (v4.2 added custom image class support) — lives there. Resist patching the published views.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log