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

Avatar Laravel Package

orchestra/avatar

Orchestra Avatar is a Laravel package for managing user profile avatars. It provides a simple API to upload, store, and render avatars, with fallbacks for users without a custom image, making it easy to add consistent avatar handling to any Laravel application.

View on GitHub
Deep Wiki
Context7

Getting Started

Before you install anything, run the compatibility gate. This is the single most important step with this package, and it takes one command:

composer show laravel/framework | grep versions
composer show orchestra/avatar --all   # inspect the require block, not the README

Documented releases stop at v6.0.0 → Laravel 8. If your app is on Laravel 9+, Composer will refuse the install or you'll be forced into a framework downgrade. Do not work around this — treat a failed constraint as the end of the evaluation and go in-house (the in-house version of this capability is roughly a validation rule, a Storage::put, a URL accessor, and a delete call).

If you are on Laravel 8, the install is three steps:

composer require orchestra/avatar:^6.0   # pin the major, commit composer.lock
php artisan vendor:publish --provider="Orchestra\Avatar\AvatarServiceProvider"
php artisan storage:link                 # if serving from the public disk

Open config/orchestra/avatar.php (or whatever the published file is named) and set the storage disk and directory explicitly. Do not rely on a default that reads env() at runtime — see the config-cache gotcha below.

Where to look first. The README is thin, so read the source. In priority order:

  1. vendor/orchestra/avatar/src/AvatarServiceProvider.php — what gets bound into the container and under what key. This is the seam you will later swap.
  2. The published config file — disk, directory, default size, Gravatar toggle, placeholder.
  3. The manager/driver class — the actual get/set/remove-shaped operations. Confirm the exact method names and return types here rather than trusting copy; the API surface is small enough to read in ten minutes.

First use case — build this one thing before anything else: a single avatarUrl(int $size = 80): string accessor on your User model that returns a local URL when an avatar exists, falls back to Gravatar (or your placeholder), and never throws. Render it once in a Blade layout header. That's the whole feature working end-to-end, and it exercises storage, fallback, and URL generation in one pass.

Implementation Patterns

Wrap it before you call it. The moment you write the first integration line, define your own interface and put the package behind it:

interface AvatarService
{
    public function url(User $user, int $size = 80): string;
    public function store(User $user, UploadedFile $file): void;
    public function forget(User $user): void;
}

Bind it in your own AppServiceProvider, not the package's. This is the entire migration strategy: replacing the package with an in-house driver or a fork becomes a binding change, not a refactor of every profile, comment feed, directory, and admin surface that renders a face.

Store the path, resolve the URL. Add a nullable avatar_path column (not a full URL — never persist hostnames, disks, or CDN domains in the DB). Resolve to a URL at render time. This lets you move from local disk to S3/CDN as a config change.

public function getAvatarUrlAttribute(): string
{
    return app(AvatarService::class)->url($this, 80);
}

Upload flow — validate, then delegate.

$request->validate([
    'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048', 'dimensions:min_width=64,min_height=64'],
]);

$avatar->store($user, $request->file('avatar'));

Keep validation in your FormRequest, not in the controller and not assumed to be in the package — the description makes no claim that validation is built in, so assume you own MIME sniffing, extension allow-listing, and size ceilings.

Replacement and deletion — you own the cleanup. Capture the old path before writing the new one, then delete it yourself rather than trusting the package:

$old = $user->avatar_path;
$avatar->store($user, $file);
if ($old) Storage::disk(config('avatar.disk'))->delete($old);

Back that with a scheduled avatar:prune command that diffs the disk against users.avatar_path — orphans accumulate from failed uploads, deleted users, and interrupted transactions regardless of how careful the happy path is.

Deterministic, collision-free filenames. Never trust the uploaded client name. Use something like avatars/{user_id}/{hash}.{ext} with a random component, then append that component as a query string for cache-busting: ...?v={$hash}. Otherwise CDN and browser caches will show the previous user's face indefinitely.

A Blade component beats a facade call in views:

{{-- resources/views/components/avatar.blade.php --}}
<img src="{{ $user->avatar_url }}" width="{{ $size }}" height="{{ $size }}"
     alt="{{ $user->name }}" class="rounded-full object-cover" loading="lazy">

Use {{ }}, never {!! !!} — the URL ultimately derives from a hashed email or a stored path, and you don't want to think about escaping later.

Expose it once via an API resource if you have a JSON front end, so the SPA doesn't reimplement fallback logic:

'avatar_url'   => $this->avatar_url,
'avatar_thumb' => app(AvatarService::class)->url($this, 40),

Test with Storage::fake(). Assert three things per test: the file lands on the configured disk, the DB column updates, and the accessor returns a non-empty string when no avatar exists (the fallback path is the one that breaks in production).

Gotchas and Tips

The Laravel 8 ceiling is the headline gotcha, not a footnote. A one-line changelog for a major framework bump tells you the maintainer does not publish upgrade notes. If you adopt on Laravel 8 as a stopgap, pair the decision with a dated fork-or-replace checkpoint tied to your next framework upgrade — not an open-ended "we'll revisit."

Gravatar is an outbound call in a render path. It sends md5(email) to gravatar.com. Three consequences:

  • For most real email addresses that hash is trivially reversible by dictionary lookup — treat it as a privacy decision, not a technical one. Get explicit sign-off for EU users and enterprise tenants, and disable it per environment at the config layer.
  • It's an N+1 outbound request in any list view. A 50-row team directory renders 50 image URLs; each browser fetch is a third party. If you must use it, make sure the placeholder branch is our branded default so non-production and demo paths make zero external calls.
  • Gravatar's default-response behaviour means a missing avatar silently returns a generic identicon — easy to mistake for "our fallback isn't working" during debugging. Append ?d=404 (or the package's equivalent) so misses are unambiguous.

Config caching will bite you. If the package's published config reads env('AVATAR_DISK') directly and you run php artisan config:cache in deploy (you should), confirm the values resolve before caching. Test with php artisan config:cache locally, not just php artisan serve.

Disk visibility and URLs. public disks give you direct URLs but require storage:link and correct filesystem permissions; private disks require signed URLs, which expire and therefore cache badly. Pick one deliberately and make it a config value, not a hardcoded string. On S3, set the bucket policy before you ship — a 403 on every avatar is the classic day-one incident here.

Do not serve user-uploaded SVG. SVG is XML and carries script. Even with an image validation rule you can be handed a polyglot file. Allow-list raster formats, and if you can, re-encode uploads through GD/Imagick to strip metadata and neutralize malformed files. Serve uploads from a path that never executes.

Don't expect resizing. The package stores and serves; it does not give you a media pipeline (no cropping, focal points, renditions, or moderation). The size parameter almost certainly shapes the URL, not the bytes. That means you're shipping multi-megabyte originals into 32px list rows unless you resize on upload yourself or put a CDN transform in front. Decide this before launch, not after the bandwidth bill.

Debug in this order:

  1. Storage::disk($disk)->exists($user->avatar_path) — is the file actually there?
  2. Storage::disk($disk)->url($path) — does the generated URL resolve in a browser?
  3. php artisan about / config('avatar') — did config cache pick up your values?
  4. composer why orchestra/avatar — confirm nothing else is dragging in a conflicting version.

Extension point. If you need to change behaviour without forking, override the container binding in your own service provider after the package's provider registers. Keep a vendor/orchestra/avatar patch list in the repo when you do fork — because with zero dependents and no maintainer response history, you should assume you are the de facto maintainer of your pinned copy.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle