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

Filament Url Image Uploader Laravel Package

amjadiqbal/filament-url-image-uploader

A Filament form field that uploads images from a file or a pasted URL, fetching and validating remote images server-side. Saves a plain storage path string, with configurable directory, disk, filename preservation, and a URL download size limit.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a small Filament 3 form field that wraps two inputs in tabs: a normal FileUpload and a URL text input with a server-side fetch action. The day-to-day value is simple: an admin can either upload an image or paste a remote image URL, and the model attribute receives a plain storage path string such as posts/images/photo.jpg. It is not a media library or a bulk importer.

Install it and pin to the 1.1 line:

composer require amjadiqbal/filament-url-image-uploader:^1.1

The 1.0.x releases had critical state-path bugs in real Filament Resource forms, so I would not start on anything older than 1.1.0. There is no config file to publish; configuration is per-field via fluent methods.

The first place to look is the component class AmjadIqbal\FilamentUrlImageUploader\Components\UrlImageUploader, plus the README usage section and the 1.1.0 changelog. Then wire it into one simple resource before rolling it out.

First use case: a Post or Product with a nullable string image column.

// migration
$table->string('image')->nullable();

// model
protected $fillable = ['image'];
use AmjadIqbal\FilamentUrlImageUploader\Components\UrlImageUploader;

UrlImageUploader::make('image')
    ->label('Featured image')
    ->directory('posts/images')
    ->disk('public');

Display it in the table and render it elsewhere through the same disk:

ImageColumn::make('image')->disk('public')->square();
Storage::disk('public')->url($post->image);

For local public disk usage, make sure php artisan storage:link has been run. Then test three flows before trusting it: create with a file upload, create with a pasted URL, and edit an existing record and save without changing the image.

Implementation Patterns

  • Use it as a normal Resource form field. Put it in your Filament form schema and keep the field name equal to the database column. In 1.1.0 it is verified against CreateRecord / EditRecord, including the conventional data form state path. Test edit-and-save-without-change explicitly, because that is where earlier versions broke.

  • Centralise disk and directory conventions. The field stores only the path, not the disk name. If different resources hardcode different disks, rendering becomes inconsistent. I would put the disk and directories in config and reuse them:

// config/media.php
return [
    'disk' => env('MEDIA_DISK', 'public'),
    'posts' => 'posts/images',
    'products' => 'products/images',
];
UrlImageUploader::make('image')
    ->disk(config('media.disk'))
    ->directory(config('media.posts'));
  • Resolve image URLs through a model accessor or presenter. This keeps table columns, Blade views, and API resources consistent:
public function imageUrl(): ?string
{
    return $this->image
        ? Storage::disk(config('media.disk'))->url($this->image)
        : null;
}

For private S3 disks, use temporaryUrl() instead of url(), and use Filament's ImageColumn::visibility('private') where appropriate.

  • Use the URL tab for one-off remote imports, not bulk imports. The component’s URL action downloads server-side, validates the content type with finfo, enforces a size cap, and stores the file. That is useful for CMS editors pasting a supplier image URL. For a catalogue import of 500 images, build an Artisan command or queued job with Http::get() plus Storage::put() and assign the resulting path directly. Driving that volume through a Livewire form is the wrong tool.

  • For a gallery, use a Repeater of single-image fields. The field explicitly does not support multiple(). If the model column is JSON and cast to array, a Repeater gives you a reasonable multi-image editor:

Repeater::make('gallery')
    ->schema([
        UrlImageUploader::make('image')
            ->disk(config('media.disk'))
            ->directory('products/gallery'),
    ])
    ->columns(1);
protected $casts = [
    'gallery' => 'array',
];

Each repeater item stores an array containing an image string path. For a fixed number of images, separate fields are simpler and easier to validate.

  • Treat validation as two layers. Direct uploads inherit Filament FileUpload image behaviour. URL fetches are checked by the package for scheme, timeout, size, and finfo content type. If an image is mandatory, test ->required() on the outer field and add a model-level rule if necessary. Do not make the inner URL tab required; the 1.1.0 fix specifically ensures a blank URL does not block a file-tab save.

  • Write a feature test around the form, not just the component. Use Storage::fake() and a Livewire form test:

Storage::fake('public');

livewire(CreatePost::class)
    ->fillForm([
        'image' => UploadedFile::fake()->image('cover.jpg'),
    ])
    ->call('create')
    ->assertHasNoFormErrors();

$post = Post::first();

$this->assertNotNull($post->image);
Storage::disk('public')->assertExists($post->image);

Also test the edit page: open an existing record, save without changes, and assert the path is unchanged and the file still exists.

  • Create a project-level field wrapper if you use it often. If many resources repeat the same disk and directory defaults, subclass the component or wrap it in a small factory. Keep the subclass thin; the package’s internal state handling is delicate.

  • Replace the URL tab with a custom action when you need more control. The package does not document support for custom HTTP headers, authenticated fetches, or domain allowlists. If those are requirements, use a Filament Action or job that downloads the image with Laravel’s HTTP client, stores it on your disk, and writes the path into the field state. That is cleaner than forking the field for a one-off need.

Gotchas and Tips

  • Pin to ^1.1 and verify the version in production. The 1.0.3 release caused No property found for validation: [image] on every Resource save because the inner fields lost the form’s data state path. The 1.1.0 release fixed this by removing the child-container override and renaming the inner fields to path and url. If you see state like image.image, or validation errors on save, you are likely on an old version.

  • It is single-image only. There is no multiple() support. Do not call it. Use one field per image slot or a Repeater of the field. If you need a true media library with ordering, captions, and conversions, this package is not that.

  • The database column should be a string, not JSON. The saved value is a plain path string. If you are replacing an existing FileUpload that stored an array or JSON, migrate the data and remove array casts for that column.

  • Keep the disk consistent between write and read. The default disk is public. If the field uses ->disk('s3'), every render path must use s3 as well. Storage::disk('public')->url($path) will produce a broken URL for an S3-stored object. For private S3, url() may not be publicly accessible; use temporaryUrl().

  • Local public disk needs the storage symlink. Run php artisan storage:link and check filesystem permissions. A missing symlink looks like a broken preview even when the file was stored correctly.

  • URL fetch has hard limits. Defaults are a 10-second HTTP timeout and a 10 MB maximum download, with only http and https schemes allowed. Slow CDNs, authenticated URLs, redirect chains, and hotlink-protected images will fail. The component does not document custom headers. For those cases, download in a job and set the path yourself.

  • Consider SSRF risk before exposing URL fetch to untrusted users. The package restricts schemes to HTTP/HTTPS and validates content type, but it does not document private-IP blocking. An admin-only CMS is one risk profile; a multi-tenant SaaS where tenants can paste arbitrary URLs is another. Add network egress controls or avoid the URL tab for untrusted users.

  • Be careful with preserveFilenames(). It is useful when importing supplier images and you want readable filenames, but it increases collision risk and relies on filename sanitisation. For user-supplied uploads, leave it off and let the package generate unique names. If you do enable it, scope directories per record or per tenant.

  • Plan for orphan files. Both direct uploads and URL fetches can write to disk before the form is saved. Abandoned forms can leave unreferenced files. Add a scheduled cleanup command that compares disk contents with database paths, or store uploads in a temporary directory and move them on save if you need strict hygiene.

  • The two tabs share one underlying state. The last successful action wins. If a user uploads a file and then pastes a URL, the result may surprise them. Test that interaction and add helper text to make the behaviour clear.

  • Do not rewrite the preview/dehydration logic casually. FileUpload keeps in-progress state as a UUID-keyed array, not a zero-indexed array. The package’s 1.1.0 view and dehydration code use Arr::first() for that reason. If you customise the Blade view, preserve that handling. Do not reintroduce afterStateHydrated overrides that replace the UUID-keyed state with a plain array.

  • Debug with the disk and the database, not just the preview. A useful checklist: confirm package version >=1.1.0; confirm the field name matches the column; confirm the disk config; run Storage::disk($disk)->exists($path); inspect the Livewire payload for data.image; check storage/logs/laravel.log for timeout or finfo failures; and run the edit-no-op test. The preview can be misleading while the stored path is actually correct.

  • Low adoption means you own the integration risk. The package has a small install base and a changelog showing rapid bug-fix cycles. That is not disqualifying for a CMS field, but I would write app-level tests around create, edit, URL fetch, and gallery repeaters before relying on it. It is MIT and small enough to fork if you need custom URL-fetch behaviour, headers, or domain allowlisting.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo