Skip to content

Media

The media package is a first-party media library — an uploadable, searchable file library with a React grid, a detail slideout for renaming and alt text, bulk delete, and a form field that attaches media to any model through a polymorphic pivot.

Terminal window
composer require lattice-php/media

That is the whole integration: the classes, migrations, config (config/media.php), and the default Media policy are picked up automatically, and the package ships its React renderer as source, which the lattice() Vite plugin compiles into your app’s bundle via virtual:lattice/plugins (see Component packages).

The package has three touchpoints.

The library component — a standalone page of media:

use Lattice\Media\Components\MediaLibrary;
MediaLibrary::make();

The picker field — the library inside a form, submitting the selected ids:

use Lattice\Media\Forms\Components\MediaPicker;
MediaPicker::make('gallery')->multiple();

The HasMedia trait — per-collection attachments on any model:

use Lattice\Media\Models\Concerns\HasMedia;
class Product extends Model
{
use HasMedia;
}
$product->syncMedia($ids, 'gallery'); // replaces the collection, keeps the given order
$product->media('gallery'); // MorphToMany<Media>, ordered by the pivot
$product->firstMediaUrl('gallery');

Validate submitted ids with Lattice\Media\Rules\AttachableMedia.

The package registers a media-image rich editor extension. Activate it per field and optionally offer conversions as selectable sizes:

use Lattice\Form\Components\RichEditor;
use Lattice\Media\Forms\RichEditor\MediaImage;
RichEditor::make('body')->withExtensions(MediaImage::make()->conversions('hero'));

The stored document keeps only {id, alt, conversion} per image — URLs are resolved on every render and prefill, so temporary/signed disk URLs work. Render stored documents as usual with RichContent::make($post->body)->toHtml().

To track usage (and benefit from per-collection conversions), sync the referenced media as attachments when you persist the document:

$post->update(['body' => $validated['body']]);
$post->syncMedia(MediaImage::idsIn($validated['body']), 'content');

Conversion names passed to ->conversions() should be generated for that collection — declare them in the model’s mediaConversions('content') so the sync dispatches their generation.

Every convertible image (jpeg, png, bmp, gif, webp) gets its derivatives generated by a queued job after upload and after attach — as does anything stored under a generic mime type, since a signed upload can record one for a real image; the job probes the bytes and skips what is not an image. $media->previewConversion() (thumb by default) is what the grid, the picker and the detail slideout preview; $media->url('thumb') reads any of them and falls back to the original when it was never generated.

Defaults live on the model. Subclass it, point media.model at your class, and return callbacks over Laravel’s immutable Illuminate\Image\Image — each one must return the transformed image. Override previewConversion() alongside defaultConversions() if the subclass drops thumb, or the library grid silently falls back to full-size originals:

class Media extends \Lattice\Media\Models\Media
{
public function defaultConversions(): array
{
return [
'thumb' => fn (Image $image): Image => $image->cover(400, 400)->optimize('webp', 70),
'hero' => fn (Image $image): Image => $image->scaleDown(1600)->optimize('webp', 80),
];
}
}

A collection can ask for more on top of the defaults. A bare string reuses a conversion that is already defined globally:

class Product extends Model
{
use HasMedia;
public function mediaConversions(string $collection): array
{
return match ($collection) {
'gallery' => ['card' => fn (Image $image): Image => $image->cover(1200, 800)],
'downloads' => ['hero'],
default => [],
};
}
}

Nothing fingerprints a callback, so an edited one is not picked up on its own:

Terminal window
php artisan media:conversions # queue every convertible media
php artisan media:conversions --missing # only what is incomplete (or has no dimensions yet)
php artisan media:conversions --force # drop the derivatives first, so new specs are adopted
php artisan media:conversions --force --only=thumb,card
php artisan media:conversions --id=12 --id=13

--only narrows what --force drops and what --missing counts as incomplete; the job itself always fills in whatever else the media is missing. Dropped derivatives are deleted from the disk, so a spec that now writes a different file extension leaves nothing behind.

The command regenerates the model’s default conversions; a collection’s extras come from the consuming model, so they are rebuilt the next time that collection is synced. Derivatives are deleted with the media — detaching a media from a record deletes nothing, because another record may rely on the same names.

Beyond the accepted types and the size cap, a library can validate every uploaded file:

MediaLibrary::make()->uploadRules(['dimensions:max_width=4000,max_height=4000']);

config/media.php covers the disk (media.disk), the upload size cap (media.max_size), the accepted mime patterns (media.accepted_types, image/* wildcards included, empty accepts everything), whether uploads go through signed URLs (media.signed_uploads), the media model (media.model) and the queue the conversion job runs on (media.queue). The previewed conversion is the model’s previewConversion(), not config — see Conversions.

A single library overrides the config defaults per instance:

MediaLibrary::make()->signedUpload()->disk('s3')->accept('image/*');

The components’ strings ship with inline English defaults. With laravel-i18next enabled, the plugin’s media namespace is loaded automatically and serves the bundled en/de translations (override them like any Laravel package translation — see Internationalization).