A Laravel package for upload storage, secure file URLs, model-based URL fields, inline preview support, cleanup tools, and browser-focused image optimization.
Laravel Uploads is built to keep file handling simple inside Laravel apps.
It gives you:
- one upload API with an optional custom folder path
- facade usage with
Uploads::upload(...) - helper usage with
GhostCompiler()->upload(...) - storage through Laravel
Storage - default file storage inside
LaravelUploads - database tracking for uploaded files
- database tracking for generated links
- model integration through one trait
- clean URL fields like
avatar - browser preview support
- forced download support with
?download=1 - image optimization with AVIF, WEBP, and original-file fallback
- optional aspect-ratio-safe resizing
- expired link cleanup command
By default, files are stored under:
storage/app/private/LaravelUploads
If you pass a custom path like:
Uploads::upload('demo/image', $request->file('avatar'));the file is stored under:
storage/app/private/LaravelUploads/demo/image
composer require ghostcompiler/laravel-uploadsIf you are developing this package locally and want to use it inside a Laravel app without publishing it to Packagist, add a path repository to your Laravel project's composer.json:
{
"repositories": [
{
"type": "path",
"url": "/absolute/path/to/laravel-uploads"
}
],
"require": {
"ghostcompiler/laravel-uploads": "*"
}
}Then run:
composer update ghostcompiler/laravel-uploadsPublish config and package migrations:
php artisan ghost:laravel-uploadsIf the files already exist, the command asks before overwriting them.
Overwrite without prompts:
php artisan ghost:laravel-uploads --forceRun migrations:
php artisan migrateThe package manages two tables:
Stores:
- storage disk
- visibility metadata
- file path
- original file name
- mime type
- extension
- size
- metadata
Stores:
- upload reference
- generated token
- expiry time
- last accessed time
Your own model still needs upload ID columns like avatar_id, resume_id, or document_id.
Example migration:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->unsignedBigInteger('avatar_id')->nullable();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('avatar_id');
});
}
};Add the trait to your model:
use GhostCompiler\LaravelUploads\Concerns\LaravelUploads;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use LaravelUploads;
protected $uploadable = [
'avatar_id' => [
'name' => 'avatar',
'type' => 'public',
'id' => 'hide',
'expiry' => 60,
],
];
}use GhostCompiler\LaravelUploads\Facades\Uploads;
$upload = Uploads::upload($request->file('avatar'));use GhostCompiler\LaravelUploads\Facades\Uploads;
$upload = Uploads::upload('demo/image', $request->file('avatar'));$upload = GhostCompiler()->upload($request->file('avatar'));$upload = GhostCompiler()->upload('demo/image', $request->file('avatar'));$user->avatar_id = $upload->id;
$user->save();$user->avatar;Uploads::remove($user->avatar_id);Or:
GhostCompiler()->remove($user->avatar_id);use App\Models\User;
use GhostCompiler\LaravelUploads\Facades\Uploads;
use Illuminate\Http\Request;
class ApiController
{
public function uploadAvatar(Request $request)
{
$user = auth()->user();
if ($request->hasFile('avatar')) {
$upload = Uploads::upload('superadmin.com', $request->file('avatar'));
$user->avatar_id = $upload->id;
$user->save();
}
return response()->json([
'user' => $user,
]);
}
}If avatar_id is mapped like:
'avatar_id' => [
'name' => 'avatar',
]then:
avatar_idstays in the databaseavatarbecomes the returned URL field
Example API response:
{
"id": "019d810d-1499-7192-9f99-4a67c5ad350b",
"name": "Ghost Compiler",
"email": "ghost@example.com",
"avatar": "https://your-app.test/_laravel-uploads/file/your-token"
}Previewable files open directly in the browser. Other files download automatically.
Preview currently supports:
image/avifimage/jpegimage/pngimage/gifimage/webpimage/svg+xmlapplication/pdftext/plain
Example preview URL:
https://your-app.test/_laravel-uploads/file/your-token
Force download:
https://your-app.test/_laravel-uploads/file/your-token?download=1
The package can optimize uploaded images globally.
When enabled:
- supported images try AVIF first
- if AVIF is unavailable, the package falls back to WEBP
- if neither conversion path works, the original file is stored
- resizing keeps the original aspect ratio
- images are never upscaled
- browser delivery becomes lighter and faster
Supported input image types:
image/jpegimage/pngimage/webp
If this config is disabled, no image conversion happens:
'image_optimization' => [
'enabled' => false,
]To actually optimize images, enable it:
'image_optimization' => [
'enabled' => true,
'quality' => 75,
'convert_to_avif' => true,
'max_width' => 1600,
'max_height' => null,
]In that example:
- width is capped at
1600 - height is calculated automatically from the original aspect ratio
Delete expired generated links:
php artisan ghost:laravel-uploads-cleanPreview how many expired links would be removed:
php artisan ghost:laravel-uploads-clean --dry-runPublished config file:
return [
'disk' => 'local',
'base_path' => 'LaravelUploads',
'defaults' => [
'type' => 'private',
'id' => 'hide',
'expiry' => 60,
],
'image_optimization' => [
'enabled' => false,
'quality' => 75,
'convert_to_avif' => true,
'max_width' => null,
'max_height' => null,
],
'preview_mime_types' => [
'image/avif',
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/svg+xml',
'application/pdf',
'text/plain',
],
'delete_files_with_model' => false,
'route' => [
'prefix' => '_laravel-uploads',
'name' => 'laravel-uploads.show',
'middleware' => ['web'],
],
];Laravel disk used for storing package files.
Base folder inside the selected Laravel disk.
Default upload visibility metadata used by the package.
Controls whether the raw upload ID field should remain visible.
Default link expiry in minutes.
Enable or disable global image optimization.
Compression quality from 1 to 100.
If enabled, supported images try AVIF first and automatically fall back to WEBP.
Optional maximum width for optimized images. If set by itself, height is calculated automatically from the original aspect ratio.
Optional maximum height for optimized images. If set by itself, width is calculated automatically from the original aspect ratio.
List of mime types that should open inline in the browser instead of downloading.
If enabled, deleting the model also deletes the stored file and related upload record.
URL prefix for generated file links.
Laravel route name used internally by the package.
Middleware applied to generated file serving routes.
- Add a Composer path repository in the Laravel app.
- Require
ghostcompiler/laravel-uploads. - Run:
composer update ghostcompiler/laravel-uploads
php artisan package:discover
php artisan ghost:laravel-uploads
php artisan migrateWhen you make changes in this package repo and want your Laravel app to use them:
composer update ghostcompiler/laravel-uploads
php artisan package:discoverIf you changed helper autoloading or package metadata, this is also useful:
composer dump-autoloadIf you changed the published config or migration stubs:
php artisan ghost:laravel-uploadsOverwrite existing published files without prompts:
php artisan ghost:laravel-uploads --forceInside the package repo:
git pullMake your changes, then:
git add .
git commit -m "Update Laravel Uploads"
git pushInside the Laravel app using the local path repository:
composer update ghostcompiler/laravel-uploads
php artisan package:discoverThis package now includes a PHPUnit + Testbench scaffold.
Inside the package repo:
composer installcomposer test- resize dimension calculation
- aspect-ratio preservation
- no upscaling behavior
- expired link cleanup command
- dry-run cleanup reporting
Use a real Laravel test project and verify:
- upload works with
Uploads::upload(...) - upload works with
GhostCompiler()->upload(...) - custom folder uploads work
- model serialization returns URL fields correctly
- preview URL opens supported file types
?download=1forces download- AVIF conversion works when supported
- WEBP fallback works when AVIF is unavailable
- resize limits preserve the original aspect ratio
- cleanup command removes only expired links
Uploads::upload($file)stores files in the configuredbase_pathUploads::upload('demo/image', $file)stores files insidebase_path/demo/imageGhostCompiler()->upload($file)uses the same Laravel Uploads service directly- generated URLs are tracked in the database
- image optimization only applies to supported images
- AVIF is tried first
- WEBP is used as the main fallback format
- resizing keeps the original aspect ratio
- GD is used first and
Imagickis used as a fallback encoder when available
Before opening a pull request, please check:
- code is clean and readable
- syntax checks pass
- tests pass
- new config options are documented
- README is updated when behavior changes
- no unrelated files are included
