Skip to content

Media

The media package is a first-party media library — an uploadable, searchable file library with a React grid, an inspector for renaming and alt text, folders, bulk actions, 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();
MediaPicker::make('gallery', 'Product gallery')
->multiple()
->maxFiles(3)

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 library renders a toolbar (search, type filter, sort order, grid or list) above the files and an inspector beside them. Clicking a file makes it the active one; the inspector shows its preview, metadata, name, alt text, folder, download and delete. Below the lg breakpoint the same panel arrives as a slideout instead of a column, and selecting several files replaces the details with a summary of the selection while the bulk bar offers the actions.

MediaLibrary::make()->inspector(false);

Every accepted file type is listed with a type icon and an extension badge. When lattice-php/pdf is installed, the library composes a viewer template and the inspector renders the selected PDF in it — compact beside the grid, with search and the page sidebar behind Open full view. Nothing to configure: install the package and PDFs preview.

Terminal window
composer require lattice-php/pdf

Folders are user-managed navigation over the same pool of files. They are metadata only — renaming or moving a folder never touches a stored file — and one global tree serves every library.

MediaLibrary::make()->folders();

The rail shows All files and Without folder above the folder tree (from lattice-php/tree, so lazy loading, keyboard navigation and drag-and-drop re-parenting come with it). Each folder node carries its own actions — new subfolder, rename, delete — and the file count as a badge. Uploads land in the folder that is open, the inspector files a single file, and Move to folder moves a whole selection.

Folders and categories answer different questions: a category is a developer-owned pool sealed into the component reference, a folder is navigation the user creates and rearranges inside whatever pool they are looking at.

A category partitions the media pool itself — unlike a collection, which groups attachments per record. Scope a library or picker to one and its listing shows only that category’s media while every upload through it is stamped with the category:

MediaLibrary::make()->category('imports');
MediaPicker::make('file')->category('imports');

An unscoped library or picker shows only uncategorized media, so a categorized pool — import files, say — never surfaces next to regular media anywhere it was not explicitly requested.

uploadOnly() skips the library entirely: the button opens the file dialog directly and the fresh upload becomes the picked value. No browse endpoint is exposed for the field, so existing media stay invisible. Combined with a category and an upload label this makes a self-contained “upload an import file” field:

MediaPicker::make('file')
->category('imports')
->uploadOnly()
->uploadLabel(__('imports.upload'));

MediaDropzone is a single-file, upload-only picker whose whole face is the file. Empty, the face is a click-and-drop target; once a file is stored the face previews it in place — a PDF in the document viewer when lattice-php/pdf is installed, an image inline, anything else as its type icon — with the remove control on the preview (in the viewer’s toolbar for PDFs). Dropping another file onto a filled face replaces it after a confirmation. The field submits the media id like a single MediaPicker, so the same rules apply:

use Lattice\Media\Forms\Components\MediaDropzone;
use Lattice\Media\Rules\AttachableMedia;
MediaDropzone::make('document', __('invoices.document'))
->category('invoices')
->height('70vh')
->emptyText(__('invoices.drop-document'))
->rules(['nullable', 'integer', new AttachableMedia]);

height() sizes the face (an int is pixels, a string any CSS length); the preview fills it. On edit, fill() the stored id and the face opens on that file. category(), uploadLabel(), and upload rules work as on the picker; multiple(), maxFiles(), and attachmentFields() are refused — use MediaPicker for those.

Both buttons a picker renders can be relabeled: pickerLabel() names the trigger that opens the browse dialog, uploadLabel() (also on MediaLibrary) names the upload button:

MediaPicker::make('file')->pickerLabel('Choose import')->uploadLabel('Upload import file');

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. Each derivative records its byte size in the conversion map alongside its path and dimensions; for maps recorded before sizes existed, media:conversions --missing backfills them from a disk stat without regenerating anything.

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).

The breakdown below measures the package’s built plugin — React and the framework runtime are external, so this is exactly what the media package adds on top of an app that already ships Lattice. It is regenerated on every docs build.

95.6 KB gzipped · 361.4 KB raw JavaScript

DependencyRawGzipShare
@lattice-php/media41.5 KB13.6 KB14.2%
TipTap + ProseMirror290.4 KB79.0 KB82.7%
Bundler runtime25.5 KB1.5 KB1.5%
w3c-keyname1.3 KB0.5 KB0.5%
orderedmap1.7 KB0.5 KB0.5%
use-sync-external-store1.0 KB0.5 KB0.5%
Other0.0 KB0.0 KB0.0%

Emitted JavaScript files

FileRawGzip
plugin.js361.4 KB95.6 KB

Open the full interactive treemap ↗ · Generated by Sonda 0.14.0 on Fri, 11 Sep 2026 17:20:05 GMT.