PHPackages                             lattice-php/media - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. lattice-php/media

ActiveLibrary

lattice-php/media
=================

First-party media library for Lattice.

0.2.0(today)020↑2750%MITPHPPHP ^8.4CI passing

Since Jul 30Pushed todayCompare

[ Source](https://github.com/lattice-php/media)[ Packagist](https://packagist.org/packages/lattice-php/media)[ RSS](/packages/lattice-php-media/feed)WikiDiscussions main Synced today

READMEChangelog (2)Dependencies (12)Versions (6)Used By (0)

Lattice Media
=============

[](#lattice-media)

First-party media library for [Lattice](https://github.com/lattice-php/lattice) — 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.

Installation
------------

[](#installation)

```
composer require lattice-php/media
```

Requires `lattice-php/lattice` `>=0.29.0 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, ordered by the pivot
$product->firstMediaUrl('gallery');
```

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

Rich editor images
------------------

[](#rich-editor-images)

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

```
use Lattice\Lattice\Forms\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.

Conversions
-----------

[](#conversions)

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 => [],
        };
    }
}
```

**Conversion names are one global namespace: one name is one spec everywhere.** Two collections that need different sizes must use different names — the generated map is per media, not per collection, so the first spec to run wins for that name. For the same reason a name that is not defined anywhere throws, and because the job resolves all the names before it generates anything, one bad name fails *every* conversion for that collection — including the ones that were fine — and burns all three attempts. Verify a new name in a queue worker's log before shipping it.

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

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

### Upload validation

[](#upload-validation)

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']);
```

The rules are sealed into the upload endpoint's context as JSON, so they travel as strings: pass string rules or rule objects that stringify (`Rule::dimensions()->maxWidth(4000)`), never closures. With `signedUpload()` the bytes go straight to S3 and the server only ever sees the object key, so rules that need the file — `dimensions` above all — apply to multipart uploads only.

Configuration
-------------

[](#configuration)

`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](#conversions).

A single library overrides the config defaults per instance:

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

Translations
------------

[](#translations)

The components' strings ship with inline English defaults. With [bambamboole/laravel-i18next](https://github.com/bambamboole/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).

Development
-----------

[](#development)

```
composer install && npm install
composer check          # pint --test, phpstan, pest (Feature)
npm run typecheck && npm test
composer test:browser   # rebuilds the workbench bundle, then runs the Playwright suite
composer serve          # workbench demo app: /media (library) and /media-picker (field in a form)
```

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance100

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/547137a6d80cad01ed1dd065b1c6af329d9a23a4134a895cff01e078cc155500?d=identicon)[bambamboole](/maintainers/bambamboole)

---

Top Contributors

[![bambamboole](https://avatars.githubusercontent.com/u/8823695?v=4)](https://github.com/bambamboole "bambamboole (95 commits)")

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/lattice-php-media/health.svg)

```
[![Health](https://phpackages.com/badges/lattice-php-media/health.svg)](https://phpackages.com/packages/lattice-php-media)
```

###  Alternatives

[bagisto/bagisto

Bagisto Laravel E-Commerce

27.9k175.2k9](/packages/bagisto-bagisto)[unopim/unopim

UnoPim Laravel PIM

10.8k2.5k](/packages/unopim-unopim)[code16/sharp

Laravel Content Management Framework

79266.1k10](/packages/code16-sharp)[october/rain

October Rain Library

1611.7M99](/packages/october-rain)[intervention/image-laravel

Laravel Integration of Intervention Image

1589.8M208](/packages/intervention-image-laravel)[hasinhayder/tyro-dashboard

Tyro Dashboard - Beautiful admin dashboard for managing Tyro roles, privileges, users, and settings

5495.1k](/packages/hasinhayder-tyro-dashboard)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
