elmudo-dev/filament-custom-attribute-file-upload
A Filament plugin for Laravel that adds file upload support to custom attributes, making it easy to attach, store, and manage uploaded files through Filament forms and resources in admin panels. Designed for Filament admin panels, it simplifies adding file fields to custom attributes without custom...
1. Confirm compatibility before anything else. The package is small (8 stars, 0 dependents), so its composer.json is the real spec. Check the Filament constraint (^3.0 vs ^2.0) and the Laravel/PHP floor. Match it to your app before running composer require; a loose constraint here is the usual source of "works on my machine."
composer require elmudo-dev/filament-custom-attribute-file-upload
2. Read the source, not just the README. For a package this size the source is the documentation. Open, in order:
src/ — the field/component class. Look at what it extends (Filament\Forms\Components\FileUpload most likely) and which public setters it exposes.resources/views/ — the Blade partial that renders the caption input next to the upload. This tells you exactly what markup and Livewire bindings you're getting.config/ and the service provider — whether there is anything to publish.3. Publish what's offered, if anything.
php artisan vendor:publish --tag="filament-custom-attribute-file-upload-config"
php artisan vendor:publish --tag="filament-custom-attribute-file-upload-views"
If the service provider registers a Filament Plugin, add it to your panel provider (->plugins([...]) in AdminPanelProvider). If it's only a form component (likely, given the description), there is nothing to register — just import the class in your Resource.
4. First use case — a captioned cover image. The canonical scenario is a Post resource where the editor uploads a cover and types a caption/alt text that travels with the file. Swap your existing field:
use ElmudoDev\FilamentCustomAttributeFileUpload\Forms\Components\CustomAttributeFileUpload;
public static function form(Form $form): Form
{
return $form->schema([
CustomAttributeFileUpload::make('cover')
->image()
->directory('posts/covers'),
// ... rest of the form
]);
}
Confirm the exact namespace and setter names against src/ — with a package this young, the README and the class name may drift. The first render should show your normal FileUpload UI plus a per-file text input for the caption.
Store the attribute where the file already lives. The component's value is only useful if the caption survives edit/reload. Two shapes are common and you should pick deliberately:
cover_meta (or similar) json column cast to array, holding ['path' => ..., 'caption' => ...].$media->setCustomProperty('caption', $value)->save(), which composes naturally if you already use SpatieMediaLibraryFileUpload.Check which one the package assumes (look at the dehydrated() / saveUploadedFileUsing() logic) before designing your migration.
Render it in Blade from a single accessor. Keep templates dumb:
// Post.php
public function coverCaption(): ?string
{
return data_get($this->cover, 'caption');
}
<figure>
<img src="{{ Storage::url($post->cover['path']) }}" alt="{{ $post->coverCaption() }}">
<figcaption>{{ $post->coverCaption() }}</figcaption>
</figure>
Multiple files / repeaters. For galleries, the field typically needs to run inside a Repeater so each item owns its caption, or the component needs to iterate state. Verify the state shape in the Livewire payload ($wire.get('data.cover')) before wiring the caption back — arrays of TemporaryUploadedFile vs stored paths behave differently on create vs edit.
Table previews. Add a TextColumn for the caption and an ImageColumn for the file, rather than trying to render the component in the table. Filament table columns are read-only; the upload component belongs in forms only.
Validation. Caption length is a content concern, not a file concern — put ->maxLength(120) and any required rules on the attribute in your form schema, and mirror them in a Form Request if the same model is written outside Filament.
Testing. Use livewire(EditPost::class)->fillForm([...])->call('save') and assert the caption column/JSON matches. Because the package adds a second input, assert on the persisted structure, not the rendered HTML.
composer.json, read the diff of any bump, and consider vendoring the component into app/Forms/Components if you only need the caption input — that removes the maintenance risk entirely.FileUpload keeps state across the delete+upload cycle. Explicitly reset the attribute when the file path changes, or you'll ship a caption describing the wrong photo.FileUpload writes to livewire.temporary_file_upload.disk first. Don't validate the caption against the temp file's existence — validate it independently.php artisan view:clear). Filament caches compiled component views in some setups, and a "my change didn't apply" bug is almost always this.->disk() / ->directory() from FileUpload; if captions "disappear," confirm storage succeeded first (Storage::disk(...)->exists($path)), then check the JSON, then the Livewire state — in that order.$wire.get('data') in the browser console for state shape, (2) the DB row for persistence, (3) ->dd() on the component's afterStateUpdated for the moment the caption attaches. The failure is nearly always between (1) and (2).->captionLabel(), and keep the base view untouched so future patches apply cleanly. If the package exposes macros, register them in a service provider so the customization is discoverable.FileUpload internals; a component that subclasses it will break silently (form renders, caption never saves). Add a test asserting the caption persists so a Filament upgrade fails loudly.How can I help you explore Laravel packages today?