---
url: /guide/models-and-blade.md
description: >-
  Attach library files to Eloquent models with HasMediaLibrary, read URLs and
  responsive images, define conversion presets, and render with Blade
  components.
---

# Models & Blade

Use the `HasMediaLibrary` trait to attach library files to any Eloquent model, and the Blade components to show them on your site.

## The HasMediaLibrary trait

```php
use Hoceineel\FilamentMediaLibrary\Concerns\HasMediaLibrary;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasMediaLibrary;
}
```

Files are grouped in named collections. A collection is just a string, such as `gallery` or `downloads`. The default collection is `default`.

| Method | Returns |
|---|---|
| `mediaLibraryItems(?string $collection = null)` | A `MorphToMany` relation, ordered. With no collection, every attached file. |
| `getMediaLibraryItems(string $collection = 'default')` | A collection of `MediaItem`. |
| `getFirstMediaLibraryItem(string $collection = 'default')` | The first `MediaItem`, or `null`. |
| `getFirstMediaLibraryUrl(string $collection = 'default', string $conversion = '')` | The first file's URL, or `null`. |
| `syncMediaLibraryItems($items, string $collection = 'default')` | Replaces the collection with the given ids or items, in that order. |
| `attachMediaLibraryItem($item, string $collection = 'default')` | Adds one file at the end of the collection. |
| `mediaAttachments()` | The raw `MorphMany` of attachment rows. |

```php
$post->syncMediaLibraryItems([4, 8, 15], 'gallery');
$post->attachMediaLibraryItem($item, 'gallery');

$post->getMediaLibraryItems('gallery');
$post->getFirstMediaLibraryItem('cover');
$post->getFirstMediaLibraryUrl('gallery', 'thumb');
$post->mediaLibraryItems('gallery')->get();
```

Sync only accepts ids that exist for the current tenant. Anything else is dropped without an error. Attachments are deleted with the model, except when the model uses soft deletes and is only soft deleted.

The [picker field](/guide/picker-field) in relationship mode calls `syncMediaLibraryItems()` for you, using the field name as the collection unless you set another.

### Avoid N+1 queries

`getMediaLibraryItems()` uses the loaded relation when it is available, so eager load it for lists:

```php
$posts = Post::query()->with('mediaLibraryItems.media')->get();

foreach ($posts as $post) {
    $post->getMediaLibraryItems('gallery');
}
```

### Storing ids in a column

You do not need the trait to store a selection. A picker in column mode saves ids in an attribute, and you read it with the Blade components below or with the model:

```php
use Hoceineel\FilamentMediaLibrary\Models\MediaItem;

$cover = MediaItem::query()->find($post->cover_id);
```

## Reading a file

Every `MediaItem` has these methods:

| Method | Returns |
|---|---|
| `url(string $conversion = '')` | URL of the original, or of a conversion. |
| `thumbnailUrl()` | The `thumb` conversion, or the original for images without one. |
| `previewUrl()` | The `preview` conversion, or the original for images without one. |
| `conversionUrlOrOriginal(string $conversion)` | The conversion if it exists. For images, the original if not. For other files, `null`. |
| `srcset(string $conversion = 'preview')` | A `srcset` string of responsive sizes, or an empty string. |
| `focalPosition()` | A CSS `object-position` value such as `32% 18%`. Defaults to `50% 50%`. |

```php
$item->url();
$item->url('hero');
$item->conversionUrlOrOriginal('hero');
$item->srcset('preview');
$item->focalPosition();
```

Public files on a public disk get direct URLs. Files on a private disk, and files marked **Only me**, get short-lived signed or temporary URLs (`temporary_url_minutes`, 30 by default). `srcset()` returns an empty string for those files, and for conversions without responsive images. See [Storage](/guide/storage).

Other useful properties on the item are `name`, `alt`, `caption`, `description`, `width`, `height`, `mime_type`, `extension` and `size`.

## Conversions

Conversions are presets in `config/filament-media-library.php`. Two ship by default:

```php
'conversions' => [
    'thumb' => ['width' => 480, 'height' => 480, 'fit' => 'crop', 'format' => 'webp', 'queued' => false],
    'preview' => ['width' => 1600, 'height' => 1600, 'fit' => 'contain', 'format' => 'webp', 'queued' => true, 'responsive' => true, 'optimize' => true],
],
```

Each preset accepts these keys:

| Key | Default | Meaning |
|---|---|---|
| `width`, `height` | none | Target size in pixels. A missing side defaults to 4096. |
| `fit` | `contain` | `crop` fills exactly `width` by `height` and needs both. Anything else fits inside the box without cropping. |
| `format` | `webp` | Output format. |
| `queued` | `false` | Generate on the queue instead of during the upload. |
| `responsive` | `false` | Also build the smaller sizes used by `srcset`. |
| `optimize` | `false` | Run Spatie's image optimizers. Slow, best queued. |

Add your own preset by adding a key:

```php
'hero' => ['width' => 2400, 'height' => 1000, 'fit' => 'crop', 'queued' => true],
```

New uploads get the conversion. Build it for existing files with:

```bash
php artisan media-library:regenerate --only=hero --missing
```

Cropped conversions centre on the focal point set in the inspector. When a focal point changes, the file's conversions are rebuilt on the queue, so keep a worker running. See [Commands](/reference/commands).

Conversions are built for images. A non-image file normally has none, and then `thumbnailUrl()` and `conversionUrlOrOriginal()` return `null`.

## Blade components

Both components are registered by the package. Outside Filament panels, enable standalone mode and load Filament's assets first: see [Outside Panels](/guide/outside-panels).

### x-media-library-image

```blade
<x-media-library-image
    :item="$post->cover_id"
    conversion="preview"
    sizes="(min-width: 1024px) 50vw, 100vw"
    class="aspect-video w-full rounded-xl object-cover"
/>
```

| Attribute | Default | Meaning |
|---|---|---|
| `item` | none | A `MediaItem`, an id, or `null`. With `null` or a missing file, nothing is rendered. |
| `conversion` | `preview` | Which conversion to use. Uses the original if it does not exist. |
| `sizes` | `100vw` | The `sizes` attribute. Only output when there is a `srcset`. |
| `lazy` | `true` | Adds `loading="lazy"` and `decoding="async"`. Pass `:lazy="false"` for images above the fold. |

Any other attribute, such as `class` or `id`, is passed to the `<img>`. The component also sets:

* `alt` from the file's alt text, or an empty string.
* `width` and `height` from the original file. Add `object-cover` and a fixed ratio class when you use a cropped conversion.
* `srcset` from the responsive sizes, when the conversion has them.
* `style="object-position: ..."` from the focal point, so a cropped image keeps the subject in frame.

Passing an item you already loaded avoids a query: `:item="$post->getFirstMediaLibraryItem('cover')"`.

### x-media-library-gallery

```blade
<x-media-library-gallery :record="$post" collection="gallery" :columns="3" aspect-ratio="4:3" />

<x-media-library-gallery :items="$post->gallery_ids" :lightbox="false" :captions="false" />
```

| Attribute | Default | Meaning |
|---|---|---|
| `record` | none | A model with `HasMediaLibrary`. Use with `collection`, or with `attribute` for a column. |
| `collection` | none | Attachment collection to show. |
| `attribute` | none | Column on `record` holding the ids. Use it instead of `collection`. |
| `items` | none | A list of ids, a single id, or an Eloquent collection of items. |
| `conversion` | `thumb` | Conversion used for the tiles. |
| `columns` | `4` | Grid columns. |
| `aspect-ratio` | `1:1` | Ratio of each tile. |
| `captions` | `true` | Show the caption, or the name, under each tile. |
| `lightbox` | `true` | Open files full screen on click. |

Give it either `items`, or `record` with `collection` or `attribute`. A `record` with neither shows nothing. The `items` form only shows files the current user can see, so other people's private files are left out.

The gallery uses Alpine for the lightbox. Pages built with `@filamentScripts` already have it. It is the same grid as the [infolist entry](/guide/infolist-entries).
