Laravel Uploader
Laravel Uploader stores uploaded files on any Laravel disk under a random hashed name and records each one in an uploads table. The Upload model gives you the right URL for the file, its readable size, an optional image thumbnail, and S3 visibility control, so a controller only needs one call to upload().
Any Disk
Store on local, public, S3 or any disk you configure, by default or per upload.
Hashed Names
Each file gets a random 67-character hash that names it on the disk and in its URL.
Thumbnails
Resize images to a thumbnail by width, height, or both, with or without the aspect ratio.
Visibility
Make an S3 file public or private and the stored object changes when you save the model.
Smart URLs
Local files use the package route, private S3 files get temporary URLs, and the rest use the disk URL.
Safe Serving
The file route sets no cookies, sends nosniff, and downloads types that could run script.
Store the upload's hash on your own models, then load the file anywhere with uploaded($hash).
#Installation
Install the package with Composer. Laravel discovers the service provider automatically, and the migration runs with your own.
#Requirements
- PHP 8.0
- Laravel 6.20 or newer within 6.x
pharaonic/laravel-readable6.x andintervention/image2.7+ (installed automatically)- The GD or Imagick extension, only to generate thumbnails
league/flysystem-aws-s3-v3, only to store files on S3
#Composer Installation
composer require pharaonic/laravel-uploader#Run Migrations
The package loads its migration automatically, so migrate creates the uploads table.
php artisan migrateTo copy the migration into database/migrations instead, publish it:
php artisan vendor:publish --tag=uploader-migrations#Publish Configuration
Publish the configuration file to config/pharaonic/uploader.php when you need to change a default:
php artisan vendor:publish --tag=uploader-configThe pharaonic and laravel-uploader tags publish both the configuration and the migration.
You're all set! Try upload($request->file('document'))->url in a controller.
#Upgrading from 4.x
The 6.x line runs on Laravel 6 with PHP 8.0. Most applications upgrade without code changes; check the points below.
#Database
A new migration makes the hash column unique. Run it after updating:
php artisan migrate#Configuration
The configuration now lives under pharaonic.uploader and publishes to config/pharaonic/uploader.php. A file you published from 4.x to config/Pharaonic/uploader.php is still applied, and config('Pharaonic.uploader') still returns the options, but both are deprecated. Move your file to the new path:
php artisan vendor:publish --tag=uploader-configWhen both files exist, config/pharaonic/uploader.php wins.
#Model traits
Pharaonic\Laravel\Uploader\Traits\Urls and Traits\Visibility were removed. If one of your own models used them, switch to the new names:
1use Pharaonic\Laravel\Uploader\Concerns\HasUrls;2use Pharaonic\Laravel\Uploader\Concerns\HasVisibility;#The file route
The route keeps its name (uploader.file) and URI (/file/{hash}), so stored links keep working. It no longer runs the web middleware group: it starts no session and sets no cookies. SVG, HTML, XML, JavaScript and unknown types are sent with Content-Disposition: attachment, so a browser downloads them instead of rendering them; they still display in an <img> tag.
#Other changes
- Uploading from a path stores the file name with its extension:
report.txt, notreport. - The
thumbnailoption is ignored for SVG files. - Exceptions are
InvalidArgumentException,LogicExceptionandRuntimeException. They extendException, so existingcatch (\Exception $e)blocks still work. - Stored paths always use
/, also on Windows.
#Configuration
Publish the file with php artisan vendor:publish --tag=uploader-config and edit config/pharaonic/uploader.php. Every option has a default, so you only need the ones you change.
1return [2 'disk' => env('FILESYSTEM_DISK', env('FILESYSTEM_DRIVER', 'local')),3 'path' => '/',4 'expire' => 30,5 'visibility' => 'private',6 'extensional' => true,7 'controller' => \Pharaonic\Laravel\Uploader\Http\Controllers\UploadController::class,8 'uri' => '/file',9];| Option | Default | Description |
|---|---|---|
disk | FILESYSTEM_DISK, then FILESYSTEM_DRIVER, then local | Disk used when an upload has no disk option. |
path | / | Base directory for every upload on the disk. |
expire | 30 | Lifetime of temporary URLs, in minutes. |
visibility | private | Visibility of new files on disks that are not local (S3). |
extensional | true | Add the file extension to local file URLs (/file/{hash}.pdf). |
controller | UploadController::class | Invokable controller of the file route. |
uri | /file | URI prefix of the file route; the hash follows it. |
#Where files are stored
Each file is stored at the base path, then the upload's own path option, then the upload date, then the hash and extension:
uploads/avatars/2026-10-28/{hash}.jpguri and controller are read when the routes load. If you change them after caching routes, run php artisan route:clear. Changing uri changes the URL of every local file already shared.
#Basic Usage
Pass an uploaded file to the upload() helper. It stores the file and returns the saved Upload model.
1use Illuminate\Http\Request; 2 3public function store(Request $request) 4{ 5 $request->validate(['document' => 'required|file|max:10240']); 6 7 $upload = upload($request->file('document')); 8 9 return [10 'hash' => $upload->hash, // "Xb3...": 67 random characters11 'url' => $upload->url, // "https://example.com/file/Xb3....pdf"12 'size' => $upload->size(), // "1.2 MB"13 ];14}The Uploader facade does the same:
1use Pharaonic\Laravel\Uploader\Facades\Uploader;2 3$upload = Uploader::upload($request->file('document'));The package stores whatever you pass it. Validate the size and type of every upload with Laravel's validation rules before calling upload().
#Upload options
The second argument overrides the defaults for one upload:
1$upload = upload($request->file('invoice'), [2 'disk' => 's3', // any disk from config/filesystems.php3 'path' => 'invoices/2026', // added under the configured base path4 'visibility' => 'public', // S3 and other non-local disks only5]);| Option | Type | Description |
|---|---|---|
disk | string | Disk to store the file on. Defaults to the disk option in the configuration. |
path | string | Directory under the configured base path. |
visibility | string | public or private. Ignored on disks that use the local driver. |
thumbnail | array | width, height and ratio of a thumbnail. See Thumbnails. |
#Upload a file from a path
upload() also accepts the path of a file on the server. The original file stays where it is.
1$upload = upload(storage_path('app/exports/report.csv'));2 3$upload->name; // "report.csv"A path that does not exist throws InvalidArgumentException.
#Find an upload again
Store the hash on your own models, then load the upload with uploaded(). It throws ModelNotFoundException, which Laravel turns into a 404, when no upload has that hash.
1$upload = uploaded($user->avatar);The Upload model is a regular Eloquent model, so you can also query it:
1use Pharaonic\Laravel\Uploader\Models\Upload;2 3$images = Upload::where('mime', 'like', 'image/%')->latest()->get();#Delete an upload
Deleting the model deletes the stored file and its thumbnail.
1uploaded($hash)->delete();#Thumbnails
Add a thumbnail option to store a resized copy of an image next to it. The thumbnail is an Upload of its own, linked through the thumbnail relation.
1$upload = upload($request->file('photo'), [2 'thumbnail' => ['width' => 300, 'ratio' => true],3]);4 5$upload->thumbnail->url; // URL of the 300 px wide copy6$upload->thumbnail->name; // "photo.jpg-thumbnail"#Sizing
| Option | Description |
|---|---|
width | Width in pixels. |
height | Height in pixels. |
ratio | true keeps the aspect ratio. Default false. |
- With
ratio, the thumbnail is scaled towidthwhen you give one, otherwise toheight. - Without
ratio, the image is resized to exactlywidth×height. A dimension you leave out keeps its original size, so give both.
1// 400 × 200 photo2['width' => 100, 'ratio' => true]; // 100 × 503['height' => 100, 'ratio' => true]; // 200 × 1004['width' => 50, 'height' => 50]; // 50 × 50A thumbnail option without width or height throws InvalidArgumentException before anything is stored.
#Which files get one
Thumbnails are made for files with an image/* MIME type other than SVG. For any other file, including SVG, the option is ignored and the file is stored without a thumbnail. The image driver must be able to read the format: GD reads JPEG, PNG, GIF, WebP and BMP, and Imagick reads more.
The thumbnail uses the same disk, path and visibility options as the original, and deleting the original deletes it too.
Thumbnails are generated with Intervention Image 2, using GD by default. To use Imagick, publish Intervention's configuration with php artisan vendor:publish --provider="Intervention\Image\ImageServiceProviderLaravelRecent" and set driver to imagick.
#URLs & Visibility
$upload->url picks the right URL for the disk the file is on:
| Disk | URL |
|---|---|
The disk named local | The package file route: /file/{hash}.{extension} |
| A private file on S3 (or any disk that supports temporary URLs) | A temporary URL, valid for expire minutes |
Anything else, such as public or a public S3 file | The disk's own URL, from Storage::disk($disk)->url($path) |
1$upload->url; // same as $upload->url()#Temporary URLs
Ask for a temporary URL directly, with the default lifetime or your own, in minutes:
1$upload->temporaryUrl(); // valid for the configured `expire` minutes2$upload->temporaryUrl(5); // valid for 5 minutes3$upload->temporary_url; // attribute form, default lifetimeThe URL makes the browser use the upload's original name. A disk that cannot create temporary URLs, such as local, throws RuntimeException.
#Visibility
On S3 and other disks that are not local, new files take the visibility from the configuration or the upload options. Change it later with public(), private() or visibility(), then save the model; the stored object is updated on save.
1$upload->public()->save();2$upload->private()->save();3$upload->visibility('public')->save();On a disk that uses the local driver, these methods throw LogicException.
#File details for APIs
info() returns the URL, or with any other target, an array that is ready to return as JSON:
1$upload->info(); // the URL, like $upload->url 2$upload->info('url', true, 10); // a temporary URL valid for 10 minutes 3 4$upload->load('thumbnail')->info('details'); 5// [ 6// 'id' => 1, 7// 'name' => 'photo.jpg', 8// 'extension' => 'jpg', 9// 'mime' => 'image/jpeg',10// 'size' => '1.2 MB',11// 'url' => 'https://example.com/file/Xb3....jpg',12// 'content' => null, // the file contents for SVG files13// 'thumbnail' => [...], // the same array for the thumbnail, when loaded14// ]info() returns the content of SVG files so you can inline them. Only inline SVG files from trusted sources: an SVG can contain script.
#The File Route
Files on the disk named local are not reachable from the web, so the package registers a route that streams them:
GET /file/{hash} name: uploader.fileThe extension in the URL is optional when extensional is on: /file/{hash}.pdf and /file/{hash} serve the same file. An unknown hash, or a file missing from the disk, returns 404.
1route('uploader.file', $upload->hash); // "https://example.com/file/Xb3..."#Response headers
| Header | Value |
|---|---|
Content-Type | The MIME type detected when the file was uploaded. |
Content-Disposition | inline for images, audio, video, PDF and plain text; attachment for everything else, including SVG and HTML. |
X-Content-Type-Options | nosniff |
The route runs no middleware group, so it starts no session and sets no cookies.
Anyone holding a local file's URL can open it. The hash has 67 random characters, so the URL cannot be guessed, but it works like a share link. For files that need access control, use a private S3 disk with temporary URLs, or a controller of your own.
#Your own controller
Point the controller option at an invokable controller to change how files are served. Middleware registered in its constructor runs on the route:
1namespace App\Http\Controllers; 2 3use Pharaonic\Laravel\Uploader\Http\Controllers\UploadController; 4 5class ProtectedFileController extends Controller 6{ 7 public function __construct() 8 { 9 $this->middleware(['web', 'auth']);10 }11 12 public function __invoke(string $hash)13 {14 return (new UploadController)($hash);15 }16}1'controller' => \App\Http\Controllers\ProtectedFileController::class,#API Reference
#Helpers
| Function | Description | Returns |
|---|---|---|
upload(UploadedFile|string $file, array $options = []) | Store an uploaded file, or a file from a path, and save its record. | Upload |
uploaded(string $hash) | Find an upload by hash; throws ModelNotFoundException. | Upload |
Both helpers are declared only when no function with the same name exists.
#Uploader facade
Pharaonic\Laravel\Uploader\Facades\Uploader, bound as pharaonic.uploader.
| Method | Description | Returns |
|---|---|---|
upload(UploadedFile|string $file, array $options = []) | Same as the upload() helper. | Upload |
options() | The resolved configuration options. | array |
route() | The file route's URI and controller, as read from the configuration. | Router |
#Upload model
Pharaonic\Laravel\Uploader\Models\Upload, stored in the uploads table.
| Method | Description | Returns |
|---|---|---|
url() | URL for the file's disk and visibility. | string |
temporaryUrl(?int $expire = null) | Temporary URL valid for $expire minutes, or the configured expire; throws RuntimeException on disks without temporary URLs. | string |
size(bool $decimal = true) | Readable size such as "1.2 MB"; false uses binary units (KiB, MiB). | string|null |
info(string $target = 'url', bool $isTemporary = false, ?int $expire = null) | The URL for url, otherwise an array of file details. | string|array |
visibility(string $visibility) | Set the visibility; apply it with save(). Throws LogicException on local disks. | static |
public() / private() | Shortcuts for visibility('public') and visibility('private'). | static |
thumbnail() | Relation to the thumbnail upload. | HasOne |
| Property | Type | Description |
|---|---|---|
id | int | Primary key. |
hash | string | Random 67-character identifier, used in URLs. |
name | string | Original file name. |
path | string | Path on the disk. |
size | int | Size in bytes. |
extension | string | Extension detected from the file contents. |
mime | string | MIME type detected from the file contents. |
disk | string | Disk name. |
visibility | string|null | public or private; null on local disks. |
thumbnail_id | int|null | ID of the thumbnail upload. |
thumbnail | Upload|null | The thumbnail upload. |
url | string | Same as url(). |
temporary_url | string | Same as temporaryUrl(). |
created_at / updated_at | Carbon | Timestamps. |
#Concerns
The model's URL and visibility methods come from two traits you can reuse on a model with the same columns:
| Trait | Methods |
|---|---|
Pharaonic\Laravel\Uploader\Concerns\HasUrls | url(), temporaryUrl(), url and temporary_url attributes |
Pharaonic\Laravel\Uploader\Concerns\HasVisibility | visibility(), public(), private() |
#Publish tags
| Tag | Publishes |
|---|---|
uploader-config, pharaonic-config | config/pharaonic/uploader.php |
uploader-migrations, pharaonic-migrations | The create_uploads_table and make_uploads_hash_unique migrations |
laravel-uploader, pharaonic | Both |
#Examples
#1. User avatars with a thumbnail
Keep the avatar's hash on the user and show the thumbnail in lists.
1Schema::table('users', function (Blueprint $table) {2 $table->string('avatar', 350)->nullable();3}); 1public function update(Request $request) 2{ 3 $request->validate(['avatar' => 'required|image|max:2048']); 4 5 $user = $request->user(); 6 7 if ($user->avatar) { 8 uploaded($user->avatar)->delete(); 9 }10 11 $upload = upload($request->file('avatar'), [12 'path' => 'avatars',13 'thumbnail' => ['width' => 96, 'ratio' => true],14 ]);15 16 $user->update(['avatar' => $upload->hash]);17 18 return back();19}1use Pharaonic\Laravel\Uploader\Models\Upload;2 3public function avatarUpload()4{5 return $this->belongsTo(Upload::class, 'avatar', 'hash');6}1@foreach ($users as $user)2 <img src="{{ $user->avatarUpload->thumbnail->url ?? asset('img/avatar.png') }}" alt="{{ $user->name }}">3@endforeach#2. Private invoices on S3
Store invoices privately and hand out links that expire after five minutes.
1public function store(Request $request, Order $order) 2{ 3 $request->validate(['invoice' => 'required|mimes:pdf|max:5120']); 4 5 $upload = upload($request->file('invoice'), [ 6 'disk' => 's3', 7 'path' => 'invoices', 8 'visibility' => 'private', 9 ]);10 11 $order->update(['invoice' => $upload->hash]);12}13 14public function show(Order $order)15{16 $this->authorize('view', $order);17 18 return redirect(uploaded($order->invoice)->temporaryUrl(5));19}#3. Publish a draft image
Uploads start private on S3; make an image public when the post goes live.
1$post->update(['published_at' => now()]);2 3uploaded($post->cover)->public()->save();#4. Return file details from an API
1Route::get('/files/{hash}', function (string $hash) {2 return uploaded($hash)->load('thumbnail')->info('details');3});#5. Import files generated on the server
1foreach (glob(storage_path('app/reports/*.csv')) as $path) {2 upload($path, ['path' => 'archive/reports']);3 4 unlink($path);5}#Troubleshooting
#Thumbnails are not created
The thumbnail option only applies to images other than SVG. Check that the file's MIME type starts with image/, and that GD or Imagick is installed: php -m | grep -Ei 'gd|imagick'.
#"Unsupported image type"
The image driver cannot read the format, such as TIFF or HEIC with GD. Switch Intervention Image to Imagick, or upload without the thumbnail option.
#"You have to set width or height thumbnail's option."
The thumbnail option needs a width, a height, or both. Nothing was stored.
#"Visibility is only available for S3."
visibility(), public() and private() only work on disks that do not use the local driver. Files on local disks have a null visibility.
#"The driver local does not support temporary URLs."
temporaryUrl() needs a disk that can sign URLs, such as S3. For local files, use url(), which points to the package file route.
#The file route returns 404
- The hash does not exist in the
uploadstable, or the file was deleted from the disk. - You changed
uriorcontrollerafterphp artisan route:cache. Runphp artisan route:clear.
#A file downloads instead of opening in the browser
The route serves SVG, HTML, XML, JavaScript and unknown types as attachments so they cannot run script on your domain. Images, audio, video, PDF and plain text open inline. An SVG still renders in an <img> tag.
#The stored extension differs from the original name
The extension and MIME type are detected from the file contents, not the client name, so photo.JPG may be stored as jpeg. The original name is kept in name.
#My published config is ignored
From 6.x, the configuration file is config/pharaonic/uploader.php. A 4.x file in config/Pharaonic/uploader.php still applies, but the new file wins when both exist. Run php artisan config:clear after moving it.
Contributors
Amazing people who made this package possible
This package wouldn't exist without the contributions of our amazing community. A huge thank you to everyone who has helped improve Uploader!
Want to Contribute?
We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.