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

Laravel Imageup Laravel Package

qcod/laravel-imageup

Laravel ImageUp is a trait that adds automatic image upload, resizing, and cropping to Eloquent models with extensive customization. Just mark image fields on your model and it handles uploads, stores paths, and deletes old images automatically.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (5 minutes):

composer require qcod/laravel-imageup
php artisan vendor:publish --provider="QCod\ImageUp\ImageUpServiceProvider" --tag="config"
php artisan storage:link

Package discovery registers QCod\ImageUp\ImageUpServiceProvider; only add it to config/app.php if you've disabled discovery. Publishing gives you config/imageup.php with upload_disk, upload_directory and auto_upload_images defaults.

Then mark the model:

use QCod\ImageUp\HasImageUploads;

class User extends Model
{
    use HasImageUploads;

    protected static $imageFields = ['avatar', 'cover']; // columns must be VARCHAR nullable
}

First use case — avatar on a profile form. Nothing else to write in the controller:

public function update(Request $request, User $user)
{
    $user->update($request->validated()); // request()->file('avatar') is picked up automatically
    return back();
}
<form method="POST" enctype="multipart/form-data"> {{-- enctype is mandatory --}}
    <input type="file" name="avatar">
    <img src="{{ $user->imageUrl('avatar') }}">
</form>

The mental model to internalize first: the trait hooks Model::saved() and reads request()->file('<field>') directly — it does not read model attributes and does not care about $fillable. Files live outside $request->all(), so mass assignment is irrelevant here. Once that clicks, the rest of the package (manual uploadImage(), setImagesField(), hooks) is just a way to feed the same pipeline from a different source.

The place to look first is the upload section of config/imageup.php plus your $imageFields array — that array is the API surface. Read CHANGELOG.md before adopting, since that's the only maintenance signal beyond the README.

Implementation Patterns

1. Start with strings, promote to arrays only when needed. ['cover', 'avatar'] is enough for a prototype. Add option arrays when a real requirement appears (resize, per-field disk, validation). Keeping every field as a big array from day one makes the model unreadable for no benefit.

2. Decide auto-upload vs. manual upload per surface. Auto-upload is right for simple CRUD forms where the input name equals the column name. Reach for the manual API for imports, API endpoints, console commands, and queue jobs:

$user->disableAutoUpload();                       // or protected $autoUploadImages = false;
$user->uploadImage(request()->file('cover'), 'cover');

Mixing them is the classic bug: auto-upload fires on the next save() and overwrites your carefully crafted manual upload. Disable it explicitly at the model or field level (['cover' => ['auto_upload' => false]]) whenever you upload manually.

3. Use one field per derivative. The package stores exactly one file per field, so the idiomatic way to get a thumbnail and a hero is two columns:

protected static $imageFields = [
    'avatar'       => ['width' => 96,  'height' => 96, 'crop' => true, 'path' => 'avatars'],
    'avatar_large' => ['width' => 600, 'path' => 'avatars'],
];

For anything more (webp/avif variants, responsive srcsets), generate derivatives yourself in an after_save hook or a dispatched job — don't try to bend the field array into a media pipeline.

4. Per-instance overrides via setImagesField(). The static property can't call functions, so multi-tenant or per-user folders get configured at runtime:

$user->setImagesField([
    'avatar' => ['path' => "users/{$user->id}/avatar", 'width' => 200, 'crop' => true],
]);

This replaces the whole field array, so spread your defaults back in or keep a protected const of base options and merge.

5. Custom filenames with the {field}UploadFilePath convention. Return a relative path; the field's path option is prepended automatically:

protected function coverUploadFilePath($file)
{
    return $this->id . '-cover.' . $file->getClientOriginalExtension();
}

The method runs after the insert (it's on saved), so $this->id is populated — this is the reliable way to get stable, human-readable paths. Keep hash names for avatars where CDN/browser caching bites.

6. Hooks are the customization seam. Use class-based hooks when you need dependencies (they resolve through the container) and closures for one-off, per-request tweaks:

class BlurFilter {
    public function __construct(private AuditLog $log) {}
    public function handle($image) { $image->blur(10); }
}

'before_save' => BlurFilter::class,
'after_save'  => function ($image) { /* file is on disk; create variants, dispatch job */ },

before_save mutates the image that gets written (you can override the configured resize there); after_save is where side effects belong — thumbnails, CDN invalidation, ImportImageJob::dispatch(...). Note that hooks receive the resized image if width/height are set; grab the original from request()->file(...) if you need full resolution.

7. Front-end cropping. Wire a JS cropper, send coordinates, and call the fluent API:

$x = (int) $request->input('crop_x');
$y = (int) $request->input('crop_y');

$user->cropTo($x, $y)->uploadImage($request->file('avatar'), 'avatar');

8. Layered validation. Keep image|mimes:jpeg,png,webp|max:2048|dimensions:max_width=4000 in your FormRequest and mirror the essentials in the field's rules option. The field rules protect manual/API uploads; the FormRequest protects the user experience (proper redirect with errors, and it fails before anything is persisted).

9. Expose URLs cleanly. Use imageUrl() inside an accessor and append it in API resources rather than sprinkling Blade calls:

protected $appends = ['avatar_url'];
public function getAvatarUrlAttribute() { return $this->imageUrl('avatar'); }

imageTag($field, 'class="rounded"') is convenient in Blade but emits raw HTML — use {!! !!} and never pass user input into the attribute string.

10. Files that aren't images. protected static $fileFields covers resumes/CVs with the same disk, path, rules, hooks and fileUrl() model — use it for consistency instead of bolting on a second upload mechanism.

Gotchas and Tips

The single biggest failure mode is a silent no-op. No exception, no log line — just no file and an unchanged column. Debug in this order: enctype="multipart/form-data" on the form; request()->hasFile('avatar'); the input name matches the field or file_input; auto_upload isn't disabled; update_database isn't false; the disk/path actually received the write (Storage::disk('public')->allFiles('uploads')); storage:link exists. Nine times out of ten it's the first or third item.

README/config drift on the disk. The README's model example shows protected $imagesUploadDisk = 'local'; while config/imageup.php defaults to public. The local disk has no public URL, so imageUrl() returns a path the browser can't load. Use public (or s3) for anything rendered in a view.

Intervention major version is the upgrade landmine. Hooks receive an Intervention image instance and call methods like blur()/resize(). The v2 and v3 APIs differ, and older releases of this package were built against v2. After any Laravel or PHP upgrade, check what composer show intervention/image resolves to and re-test every hook — hook breakage is silent at config time and only surfaces on upload.

Validation timing on auto-upload. The trait runs on saved(), i.e. after the row is written. If a field rules check fails there, you can end up with a persisted row and a failed upload. Keep FormRequest validation as the primary gate so bad files never reach the trait.

Repeated saves in one request re-upload. Because the trait inspects the live request on every save, calling save() twice (or an update() followed by a second save()) re-runs the pipeline: new hash filename, old file deleted. Wrap multi-step updates so the upload happens once, or use manual upload + auto_upload => false.

Old-file cleanup on replace works; cleanup on delete is not documented. Replacing an image deletes the previous one, but nothing in the README suggests the trait hooks deleting. Add an observer:

static::deleting(fn ($user) => $user->deleteImage($user->avatar));

Without it you accumulate orphans — especially painful and billable on S3. A scheduled sweep for files with no matching DB row is a cheap safety net.

cropTo() signature bug in the docs. The README example passes request()->only(['crop_x', 'crop_y']) (an array) into cropTo(), but the real signature is cropTo($x, $y) with two scalars. Pass integers.

cropTo() state is sticky on the instance. It's a fluent setter — the coordinates persist for subsequent uploadImage/resizeImage calls on that same model object. Reset it or use a fresh instance in loops.

crop => true needs both dimensions. Width-only is a proportional resize; setting crop with only one dimension gives you a surprising result, not a square.

placeholder is a plain URL, not a storage path. It's returned verbatim when the column is empty — so never feed it into deleteImage(), and don't expect it to live on the configured disk.

Custom filename = cache staleness; hash filename = path churn. With {id}-cover.jpg, browsers and CDNs will serve the previous image after an update. Append a version query (?v= + filemtime) in your accessor, or keep hash names and accept that URLs change on every upload.

S3/private buckets break imageUrl(). Storage::disk('s3')->url() on a private bucket yields a non-working URL. Return Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(30)) from your own accessor instead.

Synchronous processing inside the request. Intervention runs in-request; a 12 MP phone photo plus a resize plus a crop is real latency and memory. Guard with max: (KB) and dimensions:max_width rules, and move heavy variants into an after_save-dispatched job.

Testing is straightforward with fakes — and worth doing because the silent-failure modes above are exactly what regressions look like:

Storage::fake('public');
$file = UploadedFile::fake()->image('avatar.jpg', 800, 600);

$this->post('/profile', ['avatar' => $file])->assertOk();

Storage::disk('public')->assertExists($user->fresh()->avatar);

UploadedFile::fake()->image() needs GD, and resizing still exercises the real Intervention pipeline, so this catches hook and dimension-config breakage.

Extension points, ranked by leverage: hooks (container-resolved, so you can inject services — best seam for watermarking, virus scanning, audit logging), setImagesField() for runtime policy, {field}UploadFilePath for naming, custom disks for storage topology. There is no event system beyond before_save/after_save, so anything cross-cutting (cleanup, CDN purge, logging) belongs in an observer or a thin wrapper service around the trait.

Overall fit. With ~767 stars, a thin docs surface (README + changelog only) and a near-zero dependent count, treat this as a small, single-purpose utility rather than infrastructure. It's an excellent fit for CRUD avatar/cover/logo uploads with one derivative per column; it's a poor fit for media libraries, multi-variant conversions, or responsive image sets — reach for spatie/laravel-medialibrary or a hand-rolled Intervention pipeline in those cases. Keep the field configuration centralized (constants, a setImagesField() policy method) and route all side effects through hooks/observers, so swapping the package out later is a contained change rather than a grep-and-replace across your models.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console