Laravel Packagev7.0.0MIT License

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.

Quick Tip

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 7.30 or newer within 7.x
  • pharaonic/laravel-readable 7.x and intervention/image 2.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

Terminal
composer require pharaonic/laravel-uploader

#Run Migrations

The package loads its migration automatically, so migrate creates the uploads table.

Terminal
php artisan migrate

To copy the migration into database/migrations instead, publish it:

Terminal
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:

Terminal
php artisan vendor:publish --tag=uploader-config

The pharaonic and laravel-uploader tags publish both the configuration and the migration.

Installation Complete

You're all set! Try upload($request->file('document'))->url in a controller.

#Upgrading from 4.x

The 7.x line runs on Laravel 7 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:

Terminal
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:

Terminal
php artisan vendor:publish --tag=uploader-config

When 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:

app/Models/Attachment.php
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, not report.
  • The thumbnail option is ignored for SVG files.
  • Exceptions are InvalidArgumentException, LogicException and RuntimeException. They extend Exception, so existing catch (\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.

config/pharaonic/uploader.php
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];
OptionDefaultDescription
diskFILESYSTEM_DISK, then FILESYSTEM_DRIVER, then localDisk used when an upload has no disk option.
path/Base directory for every upload on the disk.
expire30Lifetime of temporary URLs, in minutes.
visibilityprivateVisibility of new files on disks that are not local (S3).
extensionaltrueAdd the file extension to local file URLs (/file/{hash}.pdf).
controllerUploadController::classInvokable controller of the file route.
uri/fileURI 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}.jpg
Route changes

uri 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.

app/Http/Controllers/DocumentController.php
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 characters
11 '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'));
Validate first

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.php
3 'path' => 'invoices/2026', // added under the configured base path
4 'visibility' => 'public', // S3 and other non-local disks only
5]);
OptionTypeDescription
diskstringDisk to store the file on. Defaults to the disk option in the configuration.
pathstringDirectory under the configured base path.
visibilitystringpublic or private. Ignored on disks that use the local driver.
thumbnailarraywidth, 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 copy
6$upload->thumbnail->name; // "photo-thumbnail.jpg" for photo.jpg

#Sizing

OptionDescription
widthWidth in pixels.
heightHeight in pixels.
ratiotrue keeps the aspect ratio. Default false.
  • With ratio, the thumbnail is scaled to width when you give one, otherwise to height.
  • Without ratio, the image is resized to exactly width × height. A dimension you leave out keeps its original size, so give both.
1// 400 × 200 photo
2['width' => 100, 'ratio' => true]; // 100 × 50
3['height' => 100, 'ratio' => true]; // 200 × 100
4['width' => 50, 'height' => 50]; // 50 × 50

A 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 keeps the original's format. Its name is the original name with -thumbnail before its own extension (photo.png gives photo-thumbnail.png), and it uses the same disk, path and visibility options as the original. Deleting the original deletes it too.

Image driver

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:

DiskURL
The disk named localThe 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 fileThe 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` minutes
2$upload->temporaryUrl(5); // valid for 5 minutes
3$upload->temporary_url; // attribute form, default lifetime

The 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 files
13// 'thumbnail' => [...], // the same array for the thumbnail, when loaded
14// ]
SVG content

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.file

The 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

HeaderValue
Content-TypeThe MIME type detected when the file was uploaded.
Content-Dispositioninline for images, audio, video, PDF and plain text; attachment for everything else, including SVG and HTML.
X-Content-Type-Optionsnosniff

The route runs no middleware group, so it starts no session and sets no cookies.

Access

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:

app/Http/Controllers/ProtectedFileController.php
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}
config/pharaonic/uploader.php
1'controller' => \App\Http\Controllers\ProtectedFileController::class,

#API Reference

#Helpers

FunctionDescriptionReturns
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.

MethodDescriptionReturns
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.

MethodDescriptionReturns
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
PropertyTypeDescription
idintPrimary key.
hashstringRandom 67-character identifier, used in URLs.
namestringOriginal file name.
pathstringPath on the disk.
sizeintSize in bytes.
extensionstringExtension detected from the file contents.
mimestringMIME type detected from the file contents.
diskstringDisk name.
visibilitystring|nullpublic or private; null on local disks.
thumbnail_idint|nullID of the thumbnail upload.
thumbnailUpload|nullThe thumbnail upload.
urlstringSame as url().
temporary_urlstringSame as temporaryUrl().
created_at / updated_atCarbonTimestamps.

#Concerns

The model's URL and visibility methods come from two traits you can reuse on a model with the same columns:

TraitMethods
Pharaonic\Laravel\Uploader\Concerns\HasUrlsurl(), temporaryUrl(), url and temporary_url attributes
Pharaonic\Laravel\Uploader\Concerns\HasVisibilityvisibility(), public(), private()

#Publish tags

TagPublishes
uploader-config, pharaonic-configconfig/pharaonic/uploader.php
uploader-migrations, pharaonic-migrationsThe create_uploads_table and make_uploads_hash_unique migrations
laravel-uploader, pharaonicBoth

#Examples

#1. User avatars with a thumbnail

Keep the avatar's hash on the user and show the thumbnail in lists.

database/migrations/2026_10_28_000000_add_avatar_to_users_table.php
1Schema::table('users', function (Blueprint $table) {
2 $table->string('avatar', 350)->nullable();
3});
app/Http/Controllers/AvatarController.php
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}
app/User.php
1use Pharaonic\Laravel\Uploader\Models\Upload;
2 
3public function avatarUpload()
4{
5 return $this->belongsTo(Upload::class, 'avatar', 'hash');
6}
resources/views/users/index.blade.php
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.

app/Http/Controllers/InvoiceController.php
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

routes/api.php
1Route::get('/files/{hash}', function (string $hash) {
2 return uploaded($hash)->load('thumbnail')->info('details');
3});

#5. Import files generated on the server

app/Console/Commands/ArchiveReports.php
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 uploads table, or the file was deleted from the disk.
  • You changed uri or controller after php artisan route:cache. Run php 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!

Moamen Eltouny

@MoamenEltouny

90 contributions

Elsayed Elbeshry

@Elsayed93

1 contribution

Oshane Bailey

@b4oshany

1 contribution

Want to Contribute?

We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.