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

Livewire Media Selector Laravel Package

drpshtiwan/livewire-media-selector

A lightweight WordPress-style media selector for Laravel and Livewire. Browse, search, paginate, and upload files from your disks, with single or multi-select, drag-to-reorder, collections, soft deletes, and trait helpers for quick model integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Verify the compatibility gate first, before you plan anything around this component. You need PHP 8.3+, Laravel 12–13, and Livewire 3.5+ or 4.x. If the project is on Laravel 11 or earlier you are pinned to the 1.x line, which predates the #[Locked] security work, the collection-scoped re-validation, and Laravel 13 support. Check with composer show laravel/framework livewire/livewire php before requiring — this is the one mistake that costs you a sprint rather than an afternoon.

composer require drpshtiwan/livewire-media-selector

php artisan vendor:publish --tag=media-selector-config
php artisan vendor:publish --tag=media-selector-migrations
php artisan migrate

php artisan storage:link

Then publish the CSS. The assets are not auto-injected — skip this and you get an unstyled modal that looks broken rather than merely ugly:

php artisan vendor:publish --tag=media-selector-assets --force

Publish views and lang files only when you actually need to customise them (--tag=media-selector-views, plus the lang tag if your locale is Arabic or Kurdish).

Where to look first, in order:

  1. config/media-selector.php — this is the real API surface. disk, directory, allowed_extensions, maxUploadKb, per_page, show_thumbnails, can_upload, table, and model all live here. Read it before writing a tag; most "why doesn't it…" questions are answered in this file.
  2. The published migration and the package Media model — this decides whether its table becomes your system of record or a staging area (see Gotchas).
  3. vendor/drpshtiwan/livewire-media-selector/resources/views — the UI is Blade you can publish. Knowing this on day one prevents you filing an upstream feature request in week three.
  4. CHANGELOG.md for whichever major you install. Three majors have shipped with documented breaking changes; config keys and security defaults have moved.

First use case — one image picker on one form, today:

<livewire:media-selector wire:model="media" collection="gallery" />
public $media; // populated by the component when the user confirms

public function save(): void
{
    $this->validate(['media' => 'required']);

    $this->post->update(['hero_media_id' => $this->media]);
}

That is a browse/search/paginate/upload/preview modal for roughly zero bespoke code, and it is the fastest way to judge whether the look and feel clears your team's bar before you commit to it.

Implementation Patterns

One component per field, each with its own collection. The same tag serves single and multi select; the difference is the shape of the wire:model payload (a single identifier for one, an ordered set for many). Render a <livewire:media-selector> per image slot on a product form — collection="hero", collection="thumbnail", collection="gallery" — and you get a coherent admin UI without building a picker per screen. Multi-select returns items in user-dragged order, so persist that order rather than re-sorting by ID.

Collections are the primary organising lever, not separate disks. collection="avatars" and collection="gallery" are scoped lists that don't bleed into each other, and selection/delete/restore are re-validated server-side against the active scope (disk + user + collection + extra scope). Keeping everything on one disk and separating by collection keeps URL generation and storage:link simple.

Let your policies drive the flags, never the browser. The permission properties are #[Locked] and resolved in mount(), so pass them from gates at the call site:

<livewire:media-selector
    wire:model="gallery"
    collection="gallery"
    :can-delete="auth()->user()?->can('delete', $post)"
    :can-upload="auth()->user()?->can('create', Media::class)"
    :restrict-to-current-user="true"
/>

For multi-tenant apps restrict-to-current-user is the difference between a per-account library and a data leak; combine it with a policy that scopes reads to the tenant as well.

Model integration goes through the trait helpers. Attach HasMediaSelector-style integration to the models that own media and use attachMedia, syncMedia, and getMediaUrl rather than hand-rolling pivot logic. syncMedia is your friend for galleries (replace the set in one call, preserving order); attachMedia for append-only use cases like attachments. The relation respects media-selector.model from config, so a custom model subclass keeps working.

React to events instead of polling. The component dispatches Livewire and browser events on upload, delete, and selection. Register a #[On('...')] listener on the parent component to refresh counters, invalidate cache, or kick off image conversions. Read the component source for the exact event names before wiring them — they aren't stable API in the way the attributes are.

Moderation workflow. Soft delete plus canSeeTrash / canRestoreTrash gives you a trash tab for free. Gate the trash tab to admins only; for regular editors pass :can-see-trash="false" so deleted assets simply vanish from their view rather than becoming a confusing purgatory.

Per-feature overrides beat global config. disk, directory, perPage, maxUploadKb, and show-thumbnails can all be set per component. Use this for the one screen that needs 10 MB PDFs while every other picker stays at 2 MB images — don't widen the global config to accommodate an exception.

i18n and RTL come along for the ride. Translations for English, Arabic, and Kurdish ship in the box, and layout keys flip automatically for RTL locales. If you need to change strings, publish the lang files rather than editing vendor.

Gotchas and Tips

  • Locked means locked. Every permission, storage, and validation property is #[Locked] and resolved once in mount(). You cannot flip canUpload from the frontend, and you equally cannot mutate these from updated() hooks or JavaScript — set them as attributes at render time. That's the security model working as intended, not a bug.
  • CSS must be published and re-published. After upgrading the package, run vendor:publish --tag=media-selector-assets --force, or you will debug a "styling regression" that is really stale assets.
  • Tailwind-only. The UI mode configuration reflects Tailwind as the only supported styling. Bootstrap or hand-rolled design systems mean publishing the views and rewriting the markup — budget for it, don't discover it in review.
  • SVG is disabled by default and should probably stay that way. Serving uploaded SVGs from a public disk is a stored-XSS vector. If a stakeholder re-enables it, sanitise on upload (enshrined/svg-sanitize) or serve with Content-Disposition: attachment. Note that an image/* MIME wildcard also matches image/svg+xml, so be explicit with allowed_extensions — extension validation follows that list.
  • Don't assume a single object payload. Multi-select returns a collection; strict single-select object mode is only planned. If you need a scalar ID from a single picker, confirm the actual payload shape in your Livewire component and normalise before saving. Guessing here produces Array to string conversion errors at runtime.
  • show_thumbnails=false is partial. It hides the selected-preview strip below Clear/Choose Media but the modal grid still renders thumbnails. If you're disabling thumbnails for performance on a large library, that's not the lever — reduce perPage or show_thumbnails per component.
  • Pagination lives on lmsPage. Anything you write that manipulates the page — custom filters, query strings, reset buttons — must use that property name. The package's own trash and delete actions reset it consistently; your code should too.
  • If you swap media-selector.table or model, change both sides. The migration, the model, and the trait relation must agree. Changing the table after migrating means republishing and re-running the migration (or an ALTER) — do it in the first hour, not the third week.
  • Upload limits are two limits. maxUploadKb is enforced by the component, but PHP's upload_max_filesize and post_max_size sit above it. If uploads fail silently with no validation message, check the PHP ini before the package.
  • Keep the collection stable across a model's lifetime. Selections are re-validated against the active scope, so a media_id written under one collection and read under another will be rejected — the protection is real, but it presents as "my image disappeared" rather than an error.
  • Debug with the standard Livewire tools. #[Locked] violations surface in the Livewire error payload, not as a flash message; watch the network tab for the failed round-trip. For missing previews, inspect the stored path and disk — preview rendering uses safe URL/MIME helpers now and won't throw on missing files, so broken cards mean stale rows, not a rendering bug. Prune orphaned records with a scheduled command.
  • Pin the version and read the changelog before every upgrade. Three majors, each with security-relevant changes. Only 4.x carries the #[Locked] work and Laravel 13 support; there is no backport path for older frameworks.
  • The component authorises actions, not your data write. It will never grant a delete you didn't enable, but you still own the check that the ID returned in wire:model belongs to the record you're saving. Validate the selection against your own scoped query before persisting.
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.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk