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

Media Library Laravel Package

artisanpack-ui/media-library

A Laravel media library for handling uploads, storage, and image conversions, with a UI-friendly API for attaching and managing files across your application.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the package and run its migrations. If you're coming from anything before v1.5.0, that release added six columns to the media table (optimization_status, optimized_at, optimization_bytes_saved, optimization_original_size, optimization_formats, optimization_error), so an existing install needs php artisan migrate before anything else. v1.6.0 ships no migrations and no PHP-side breaking changes — it's a JS/composable release, so a composer update + npm update is the whole upgrade.
  2. On a fresh or upgraded library, run php artisan media:backfill-optimization-status once. It marks pre-existing image rows as optimized so legacy media doesn't render as pending in the status UI. Without it, every historical upload looks like it's stuck in the queue.
  3. First use case: upload a file through any entry point (the helper, the InteractsWithMedia trait, the Livewire component, or the HTTP controller — they all funnel through MediaUploadService::upload()), then read the optimization block off MediaResource to render a badge.
$media = media()->upload($request->file('image')); // or $model->attachMedia(...)
// $media->optimization_status === 'pending' until processImage() runs
  1. First frontend use case (new in v1.6.0): if you render the picker inside a modal, drawer, or tab, pass enabled and stop paying for /api/media on page load.
// React
const picker = useMediaPicker({ enabled: isOpen });
// Vue
const picker = useMediaPicker({ enabled: computed(() => isOpen.value) }); // ref or getter

If you're wiring a downstream consumer (alt text, tagging, transcription), start with the MediaUploaded event rather than the ap.mediaLibrary.uploaded hook — it's the queue-safe surface.

Implementation Patterns

Queue listener on MediaUploaded. The whole point of the new event is that the upload request doesn't wait on you. Always implement ShouldQueue:

use ArtisanPackUI\MediaLibrary\Events\MediaUploaded;
use Illuminate\Contracts\Queue\ShouldQueue;

class GenerateAltText implements ShouldQueue
{
    public function handle(MediaUploaded $event): void
    {
        if (! str_starts_with($event->media->mime_type, 'image/')) {
            return;
        }
        // call your AI provider, write back to the media model
    }
}

Register it in EventServiceProvider or via Event::listen(MediaUploaded::class, GenerateAltText::class). Because it fires from the service and not from a controller, you get the same behaviour for API uploads, Livewire uploads, and programmatic ones — no need to duplicate the dispatch in each controller.

Pick the right surface: hook vs. event. The synchronous ap.mediaLibrary.uploaded hook still fires first and is the place for in-request work (validation side effects, immediate DB writes you need before the response). The event is for anything slow or third-party. Don't move fast work into the listener just for consistency — you'll add queue latency for no benefit.

Optimization status as a first-class state machine. Treat optimization_status as the single source of truth rather than probing optimization_formats for emptiness. The enum is pending → optimized on success, pending → failed on any pipeline exception, with non-image rows sitting at whatever "N/A" maps to (nulls in the resource block). A badge component becomes trivial:

$opt = $mediaResource['optimization'];
// ['status' => ..., 'optimized_at' => ..., 'bytes_saved' => ..., 'formats' => [...], 'error' => ...]

Rendering savings. optimization_bytes_saved and optimization_original_size let you show "1.2 MB → 380 KB" without re-reading files from disk or re-running getimagesize(). Compute the percentage in the presentation layer; the raw numbers are stable.

Frontend contracts. With OptimizationStatus and MediaOptimization exported as TypeScript types, generate or hand-copy them into your resources/js/types and type the props of your badge component. If you're using Inertia or a JSON API, the MediaResource shape is now the contract — mirroring it in TS catches drift when the package adds columns.

Gate the picker with enabled (v1.6.0). This is the new default posture for anything mounted-but-hidden. MediaModal already threads its open state through, so if you use the shipped modal you get this for free — do nothing. Reach for the option when you're building your own trigger:

// React — deferred until the drawer opens, loads once
const { media, refresh } = useMediaPicker({ enabled: drawerOpen });
// Vue — must be reactive; a plain boolean won't flip
const drawerOpen = ref(false);
const { media } = useMediaPicker({ enabled: drawerOpen });      // ref works
const { media } = useMediaPicker({ enabled: () => drawerOpen.value }); // getter works

The fetch happens on the false → true transition, once. Closing and reopening does not refetch unchanged filters, which makes it safe to leave the picker mounted across a long-lived page instead of tearing it down.

Defer-and-replay instead of imperative pre-warming. Changes made while enabled is false are queued, not dropped: page changes and refresh() calls issued while closed are applied on the next open. That means you can wire a "Recently uploaded" widget elsewhere on the page to call refresh() blindly without checking visibility — the picker handles it. Don't build your own open-tracking guard around it; you'll double up.

Backfill as a deploy step. Add media:backfill-optimization-status to your deploy script (after migrate, before queue workers restart). It's idempotent in intent — it flips rows that are image-like into optimized — so re-running it is safe, but it is not a re-optimization pass; it does not touch bytes on disk.

Bulk/legacy migration pattern. If you're importing an existing DAM, upload through the service so pending is set naturally, then let processImage() transition each row. Only run the backfill for rows you deliberately want to declare "already handled."

Gotchas and Tips

  • MediaUploaded is dispatched inside the request. It fires immediately after the sync hook, still in the upload lifecycle. If your listener forgets ShouldQueue, you've just added your AI provider's latency to the upload response. There's no guard against this — make it a code-review checklist item.
  • Optimization completion is not guaranteed before your listener runs. The event is dispatched at upload time; MediaProcessingService::processImage() flips pending → optimized afterwards. A queued alt-text listener can execute before the status settles. If your job needs the optimized derivative, check optimization_status === 'optimized' and release/retry, or hook into the optimization path rather than the upload event.
  • Failures still throw. The failed status is recorded, but the pipeline exception is re-surfaced. If a failed optimization shouldn't fail the whole upload (common when you accept any file and optimize best-effort), catch around your own call site — don't assume the package swallows it.
  • Nulls, not omitted keys. The optimization block is present with null values for non-image rows so the shape stays stable. Guard in your UI (v-if="opt.status") instead of optional-chaining into a missing key.
  • Backfill is a labelling operation, not a repair. Existing rows get marked optimized regardless of whether they actually have generated formats. If you need true numbers for legacy media, you must re-run optimization yourself and update the columns.
  • Migration size. Six new columns on a potentially large media table. On a big library, run the migration in a low-traffic window and confirm your DB doesn't rewrite the table (MySQL 8 / Postgres are fine; older MySQL with ALGORITHM=COPY is not).
  • Cache and indexers. If you cache serialized MediaResource payloads or index them into a search engine, the new optimization block is stale until you invalidate. Bump your cache key or add a model observer on optimized_at.
  • enabled means "no data yet", not "empty results". With enabled: false, the picker has never fetched — so media, recentlyUsed, and pagination metadata are uninitialised, not [] of a known-empty library. Render a skeleton keyed off enabled/loading state, and never branch on "length === 0" to show an empty-state message before the first fetch lands.
  • Vue: pass a reactive source, not a snapshot. useMediaPicker({ enabled: isOpen }) where isOpen is a plain boolean destructured at setup time captures the initial value forever. Use a ref or a getter; if the picker never loads, this is the first thing to check.
  • Version skew silently disables enabled. The PHP package and the JS package version independently. An older JS bundle ignores the unknown option rather than erroring, so a mismatched frontend looks exactly like the bug #98 fixed — mounted-closed modal still hammering /api/media. Pin both, and verify by watching the network tab, not by reading the option in your source.
  • Remove your StrictMode workarounds. useMediaLibrary now honours autoFetch: false across React 18 StrictMode double-invocations. If you were suppressing the duplicate fetch with a useRef latch, an effect cleanup hack, or by moving the call out of useEffect, delete it — it's now redundant and can suppress the legitimate second fetch after a real remount.
  • Remove your Vue first-filter-change workaround. The Vue composable no longer swallows the first filter change when autoFetch is false. If you compensated by firing an explicit initial fetch or by seeding filters in onMounted, you'll now fetch twice on mount.
  • enabled doesn't reset on close. Reopening with unchanged filters serves the cached result set. If you expect fresh data on every open (e.g. media uploaded by another user mid-session), call refresh() on the open transition yourself.
  • The cursor-pointer / hover / focus-visible change is purely cosmetic. No API impact. If you'd previously patched the package's button styles to work around the missing affordance, remove your overrides or you'll get doubled focus rings.
  • Release-workflow docs import is not your concern unless you fork the repo: DOCS_API_TOKEN gates the post-release docs/changelog push to the ArtisanPack UI docs site, and the step is skipped when unset. No runtime impact.
  • Extension points worth knowing: the ap.mediaLibrary.uploaded action hook (synchronous), the MediaUploaded event (queueable), MediaResource (presentation shape), MediaProcessingService::processImage() (optimization pipeline), and — new — the enabled option on useMediaPicker (React/Vue fetch gating). The PHP service is the chokepoint — every upload path routes through it, which is why the event is reliable across helper, trait, Livewire, and HTTP entry points.
  • Debugging status flips: if a row is stuck on pending, the job never reached processImage(). Check the queue connection for the processing job before assuming the pipeline broke — failed is only written when an exception is actually thrown inside the pipeline.
  • Debugging stray /api/media requests: check whether the picker is mounted closed without enabled, then check JS package version, then check whether you're using MediaModal (which should handle it). A request fired before the first user interaction is the tell.
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.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle