wotz/filament-image-or-video
A Filament form field that accepts either an image or a video upload in one component, switching previews automatically and keeping validation simple. Drop it into any resource form, repeater, or custom page for flexible media input.
Read this first: the package is effectively unvetted. Zero stars, zero dependents, and a single release means you are the first reviewer. Before you write a line of application code, do a source audit:
composer require wotz/filament-image-or-video
composer show wotz/filament-image-or-video
Check composer.json for the Filament constraint it was built against (v3 vs v4 namespaces differ: Filament\Forms\Components vs Filament\Schemas\Components, Filament\Tables\Columns vs Filament\Tables\Columns with new ->image() semantics). A mismatch here is the most common reason a small Filament plugin "doesn't work" — autoload succeeds, the class resolves, and the field silently renders nothing.
Where to look, in order:
src/ — find the concrete Filament classes. Expect something like a form field (ImageOrVideo), a table column (ImageOrVideoColumn), and an infolist entry (ImageOrVideoEntry). Confirm the exact names; do not trust the README alone.resources/views/ — the Blade template is the actual rendering logic. This is where the image-vs-video decision lives. Read it before trusting any option.composer.json — Filament major version, PHP floor, and whether the service provider is auto-discovered (extra.laravel.providers).README.md — last, because at this adoption level it is usually aspirational.If it ships assets: php artisan vendor:publish --tag=filament-image-or-video (or filament:assets if it registers CSS via Filament's asset pipeline).
First use case — the one that justifies the dependency. A Post model with a single media column that accepts either a hero image or a short MP4. Upload path stays a normal Filament FileUpload; the package is used purely for rendering in the table, the infolist, and the public view:
// app/Filament/Resources/PostResource.php
use Filament\Forms\Components\FileUpload;
FileUpload::make('media')
->disk('public')
->directory('posts')
->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp', 'video/mp4'])
->maxSize(51200)
->required(),
Then add the package's field/column beside it. That split — upload handled by core, display handled by the package — is the sane integration point, because it means a package failure degrades to "no preview," not "no uploads."
Pattern 1 — Form field as preview, not upload. If the package field wraps its own upload, resist using it as the only uploader. Wire it to the same state key as your FileUpload, or place it after and make it ->dehydrated(false). Keeping core FileUpload in charge of validation, disk, and cleanup means you can swap the package out in an afternoon.
Pattern 2 — Table column with type-aware thumbnails. The value is that one column serves a heterogeneous media set:
use Wotz\FilamentImageOrVideo\Tables\Columns\ImageOrVideoColumn; // confirm namespace
ImageOrVideoColumn::make('media')
->disk('public')
->height(48),
For a video, expect a static poster or a <video> that must be preload="metadata" and muted to avoid a wall of autoplaying frames in a 50-row table. Verify which behaviour you actually get by inspecting the Blade file — this is the single most likely place the package surprises you.
Pattern 3 — Infolist / view page entry. Same shape as the column. This is where a real <video controls> is appropriate, since the user is looking at one record. If the package only exposes a column, wrap the Blade partial in a custom ViewEntry rather than fighting the API.
Pattern 4 — Centralise the type decision. Don't let the package be the only thing that knows image-vs-video. Add a tiny helper or model accessor so the rest of the app (API resources, sitemap, OG tags) agrees with the UI:
public function isVideo(): bool
{
return str_starts_with((string) $this->media_mime, 'video/');
}
Store the mime type at upload time via FileUpload::make('media')->storeFileNamesIn(...) or a model observer. Deriving it from the file extension at render time is where these packages typically cut corners (image/svg+xml, .mov, uppercase extensions).
Pattern 5 — Disk and visibility consistency. Pass the same ->disk() and ->visibility() to the package as to your FileUpload. A mismatch yields a 403 on a private S3 disk and a broken <img> with no exception thrown — the classic silent failure.
Pattern 6 — Reuse on the front end. The package's Blade partial is usually usable outside the panel via @include('filament-image-or-video::...') or a published view. That is often more valuable than the Filament field itself, because it keeps panel and public rendering identical. Publish and own the view if you go this route.
Pattern 7 — Test the contract, not the package. A feature test asserting your resource page returns 200 and contains the expected file URL will catch a Filament upgrade breaking the plugin, which is the realistic failure mode here:
$this->actingAs($admin)->get(PostResource::getUrl('index'))->assertOk();
Low adoption is the headline risk. No dependents means no one has hit the Filament v4 upgrade for you. Pin the version ("wotz/filament-image-or-video": "1.0.*") and budget for vendoring the Blade template if the maintainer goes quiet. Check the release date against your Filament version — a package built for v3 will autoload fine into a v4 app and then fail on getState()/$getRecord() signature changes.
Temporary upload URLs. Filament's FileUpload serves a Livewire temporary URL for freshly selected files and the disk URL after save. Many small plugins only handle the second. If the preview is blank until you hit Save, that's the bug — look for a temporaryUrl branch in the view.
S3 and signed URLs. Private buckets return expiring URLs. A cached table column, an emailed notification, or a queued export built on that URL will 403 hours later. For anything persisted outside the request, store the path and resolve at render time.
Videos are heavy. Never let a table column emit autoplay or a full <video> per row. Check for preload="none" or a poster image. If the package doesn't support a poster, extend it or wrap it — a page with 25 MP4 elements will stall the browser regardless of how small the files are.
Mime detection. Confirm whether the package uses pathinfo() (extension-based) or mime_content_type()/Storage::mimeType() (content-based). Extension-based fails on .mov, .m4v, .ogv, and files served without extensions from S3. Content-based costs a storage round-trip per render — unacceptable in a table. The right answer is a stored mime_type column; if the package doesn't offer it, add one.
Null and empty-state handling. Verify ->defaultImageUrl() or equivalent exists, and that null media doesn't throw. With zero dependents, untested null paths are likely.
Filament asset registration. If thumbnails are unstyled, you skipped the asset publish/filament:assets step or the plugin registers CSS under a v3 tag Filament v4 no longer emits. Inspect the rendered <head>.
Extension points to prefer. Published views and a subclassed column beat forking. If the class isn't final and the render logic lives in a protected method or a view, you can override cleanly and keep composer updates working. Read for final before planning around it.
Debugging checklist, in order: (1) does the class namespace match your Filament major version; (2) does the stored value match what the field expects (path string vs array vs FileAttachment object — a mismatch is silent); (3) does the resolved URL load when pasted directly in a browser; (4) does the Blade view handle the video/ branch at all. Steps 2 and 3 resolve most reports.
Exit strategy. Keep the package to one wrapper class in your codebase. If it's abandoned, you replace one file with a 20-line Blade component doing str_starts_with($mime, 'video/') — provided you never let its API leak across resources.
How can I help you explore Laravel packages today?