tomshaw/laravel-gravatar
Gravatar avatars, profiles, and QR codes for Laravel. Zero config to start, configurable when you need it, with a Blade directive and component for rendering, a facade and fluent builder for URLs, plus model support, profiles, QR codes, and hashing.
1. Install — nothing else.
composer require tomshaw/laravel-gravatar
No provider registration, no migration, no config publish. Defaults are already sane (size 80, mp default image, rating g, secure URLs, SHA256 hashing).
2. Verify the requirement floor before you commit. The README lists PHP 8.5 and Laravel 13.0. This is the single biggest adoption gate — on a PHP 8.3/Laravel 11 app this package simply will not install. Check composer.json constraints on the repo/packagist before promising it to a team.
3. First use case: a global avatar in a layout. Reach for the component first — it emits the entire <img> and handles things a URL string cannot:
<x-gravatar :email="auth()->user()->email" :size="40" class="rounded-full" />
The directive is the fallback for when you own the markup string (e.g. inside a src attribute you're building conditionally):
<img src="@gravatar(email: $user->email, size: 60, default: 'retro')" alt="">
4. Where to look first.
php artisan vendor:publish --tag=gravatar-config
Gravatar::src('[email protected]') and Gravatar::hash('[email protected]') are pure string building. Only has() and profile() touch HTTP.5. The 60-second smoke test (no Gravatar account needed):
use TomShaw\Gravatar\Facades\Gravatar;
Gravatar::src('[email protected]', size: 200, default: 'retro'); // string
Gravatar::for('[email protected]')->size(200)->identicon(); // Stringable
Gravatar::qrCode('[email protected]', size: 350); // URL
| Context | Use |
|---|---|
| Blade layouts, tables, comment lists | <x-gravatar /> — forwards attributes, adds 2x srcset, has onerror fallback |
| Custom markup you already control | @gravatar directive (URL only) |
| PHP: mailables, API resources, notifications, jobs, JSON payloads | Gravatar::src() / fluent builder |
| Domain objects with an email column | HasGravatar trait |
The decision rule: if you want the <img> tag, use the component; if you want a string, use the facade or builder. Mixing the two (building URLs by hand and string-concatenating parameters) throws away the validation that makes this package worth having.
Attach the trait, then align the column once in config (or per model if you have outliers):
class User extends Authenticatable
{
use HasGravatar;
protected function gravatarEmailColumn(): string
{
return 'contact_email';
}
}
$user->gravatarUrl(size: 200, default: 'retro');
$user->gravatarProfileUrl();
$user->gravatarQrCode(size: 350);
Keep the gravatar-prefixed method names. Never add a gravatar() method to the trait or model — Eloquent routes bare property access through relations and mutators, so $user->gravatar (and therefore any $user->gravatar->something) blows up with a LogicException. When you want the fluent builder on a model instance, start from the facade:
Gravatar::for($user->email)->size(120)->robohash()->url();
Avatar sizes proliferate and drift (48 here, 50 there, w-12 in CSS but size: 60 in PHP). Define them once:
enum AvatarSize: int
{
case Nav = 32;
case List = 48;
case Profile = 200;
}
<x-gravatar :email="$user->email" :size="AvatarSize::List->value" class="size-12 rounded-full" />
Because the component also sets width/height attributes from size, this keeps the rendered box and the source image in sync and avoids layout shift.
has() and profile() are the only network calls, and they cache — including misses. So:
Gravatar::profile() inside a Blade loop without pre-warming or chunking; a 50-row table is 50 potential cache hits/misses.Cache::remember wrapper in your own service for pages that need full profiles.$profile = Gravatar::profile($user->email); // ?Profile
$title = $profile?->jobTitle ?? $user->title; // always null-safe
Profiles work with no API key, but Gravatar withholds fields from anonymous callers (timezone, languages, contact info, links, interests, payments, gallery) and caps verified accounts at four. Treat every optional field as absent-by-default and reach for get() for anything not surfaced as a property:
$profile?->displayName;
$profile?->verifiedAccounts; // may be truncated without a key
$profile?->get('timezone'); // null for anonymous callers
$profile?->toArray(); // inspect the raw payload when a field "doesn't exist"
Set GRAVATAR_API_KEY in .env only if you actually render those fields — the key buys you full payloads and a higher rate limit.
The builder is the nicest fit when the default image depends on your own data, since each Gravatar default has a shorthand:
$url = Gravatar::for($user->email)
->size(200)
->when($user->has_avatar, fn ($b) => $b->mp(), fn ($b) => $b->initials())
->rating('pg')
->url();
It's Stringable, so it echoes straight into Blade without ->url():
<img src="{{ Gravatar::for($user->email)->size(120)->identicon() }}" alt="">
Also useful for pre-flight checks before committing to a layout:
if (Gravatar::for($email)->exists()) { /* ... */ }
Gravatar::fake() replaces has() and profile() with canned data. URL building is untouched — correctly, because it never hit the network.
$fake = Gravatar::fake(
profiles: ['[email protected]' => ['display_name' => 'Ada Lovelace']],
existing: ['[email protected]'],
);
Two things to internalise:
profiles also report as existing — you don't need to duplicate them in existing.$fake->assertLookedUp('[email protected]');
$fake->assertNothingLookedUp();
Put Gravatar::fake() in a shared test setup so a stray profile call in a feature test can't turn into a real HTTP request in CI.
Useful beyond novelty: profile cards, printed badges, invite screens, "share my card" modals.
Gravatar::qrCode($user->email, size: 350, version: 3, type: 'gravatar');
version is 1 (standard) or 3 (modern dots); type is default, user (their avatar in the centre), or gravatar (the logo).
default and fallback are not the same thing — and you probably want both. default is rendered by Gravatar when the address has no avatar, so it still requires Gravatar to be reachable. fallback is what the browser shows when the request fails outright (DNS, outage, dropped connection). Configure fallback as a local asset path and it resolves through asset():
'fallback' => '/img/avatar-placeholder.png',
forceDefault is a string in some places and a boolean in another. The directive / src() take 'y' or 'n'; the component's force-default prop is a real boolean. Passing true to src(forceDefault: true) or "y" to the component will not do what you expect — and the parameter validator may reject it.
Validation throws — plan for it. Anything outside the accepted parameter set raises InvalidArgumentException. That's a feature for typos in your own code, but it's a production hazard if size, rating, or default ever come from a request parameter. Clamp and whitelist at the boundary:
$size = max(1, min(2048, (int) $request->integer('size', 80)));
Hashing: SHA256 is the default and it's the right one. Gravatar now specifies SHA256 and explicitly warns against MD5 (reversible, leaks the address). Use MD5 only if you have pre-existing URLs generated the old way — Gravatar::for($email)->md5()->url() or a global 'hash' => 'md5'. Don't add a "let the user pick the hash" option; there's no legitimate UI reason.
Retina srcset silently disappears above 1024px. The component skips the 2x variant when doubling would exceed Gravatar's 2048px ceiling. If a large hero avatar looks soft on a retina display, that's why — it's correct behaviour, not a bug.
Cached misses mean a new Gravatar account won't appear immediately. Because has() and profile() cache negative results, a user who just signed up at gravatar.com keeps showing the default image until the TTL expires. Either accept the hour, shorten gravatar.cache.ttl, or provide an explicit bust path. Setting ttl to 0 disables caching entirely — do that only in local/dev, or you'll hammer the API on every page.
Normalise emails before they reach the package. Trim it, and be aware Gravatar's identity is the lowercased, trimmed address. If you store mixed-case emails and the user registered with lowercase, you'll get a miss you can't explain from the UI. Normalise at the model/accessor level rather than sprinkling strtolower() at every call site.
Publishing the view is a maintenance commitment. php artisan vendor:publish --tag=gravatar-views gives you the <img> markup, but from then on you own it — package updates to that view won't reach you, including the srcset and onerror logic. Publish only if you genuinely need different markup, and keep the diff minimal so you can eyeball future changes. class and other attributes already forward through, so most styling needs no publish at all.
profile() returns null, not an empty object. Always ?->. Likewise, fields withheld from anonymous callers are null — a $profile->location that "doesn't work" is usually "no API key", not a bug.
Clear config cache after publishing (php artisan config:cache in deploy) — a published config that's been cached will shadow any .env change to GRAVATAR_API_KEY.
Low bus factor — put a seam around it. With one star and zero dependents, this package is a single-maintainer dependency. That's fine for avatars (trivially replaceable), but only if you don't let it leak into fifty Blade files. Consider a thin app-level helper (Avatar::url($user, AvatarSize::List)) that wraps the facade, so swapping the package later is one file rather than a codebase-wide grep. Avoid depending on TomShaw\Gravatar\... classes outside that seam — Profile is the one you're most likely to type-hint, and it's the one you'd have to reimplement.
Extension points, ranked by leverage: published config → the gravatarEmailColumn() override on models → published view → your own wrapper service. There's no custom default-image registration hook, so anything more exotic (S3-backed avatars, third-party providers) belongs in your wrapper, not in the package's config.
How can I help you explore Laravel packages today?