awcodes/filament-gravatar
A Filament plugin for displaying Gravatar avatars in your admin panel. Add Gravatar support to user tables and profiles with a simple component, letting Filament show globally recognized avatars based on email addresses.
Confirm the API before writing code. The evaluation materials for this package are inconsistent — some notes reference creativeorange/gravatar (a facade with Gravatar::get() and a published config/gravatar.php), while the package itself is a Filament avatar provider. Before you estimate anything, open src/ in the repo and confirm three things: the plugin/provider class names and their namespace, the config key names, and the exact method the provider implements on Filament's AvatarProvider contract. Everything below assumes the standard shape (GravatarPlugin::make() / GravatarProvider), but treat those names as "verify on install."
Install and pin.
composer require awcodes/filament-gravatar:^4.2.1
>=4.2.1 is the floor, not a preference — v4.2.0 shipped an mb_trim() call (PHP 8.4 only) against a declared ^8.2 constraint, which fatals on 8.2/8.3 when the lockfile resolves an older symfony/polyfill-mbstring. Pin exactly and commit the lockfile.
Check whether you need it at all. Filament core already ships Filament\AvatarProviders\GravatarProvider. Before adopting a dependency, try:
$panel->defaultAvatarProvider(\Filament\AvatarProviders\GravatarProvider::class)
If that renders the avatars you want with no config, you're done — 32 stars and a single maintainer is not worth paying for a d= parameter. Adopt awcodes/filament-gravatar only if you actually need its config surface (size, default/fallback image, rating) or a per-panel toggle.
First use case. Register on an internal admin panel only:
use Awcodes\FilamentGravatar\GravatarPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->id('admin')
->plugin(GravatarPlugin::make());
}
That single call replaces the provider everywhere the panel resolves an avatar — the top-right user menu, Tables\Columns\ImageColumns wired to avatars, AvatarEntry in infolists, relation managers, and the profile page. You should not need to touch resource or table code at all. If you do find yourself editing resources to make avatars appear, you've mis-registered the provider — go back to the panel class.
Where to look first: the plugin's config file (php artisan vendor:publish --tag="filament-gravatar-config") and the provider class itself. It's a thin URL builder; reading it end-to-end takes five minutes and answers the email-normalization question that otherwise bites you later.
The highest-value pattern for day-to-day operation. Put the provider behind config so a compliance or network complaint is a .env change plus config:cache, not a deploy:
// config/filament.php or your panel provider
$panel->defaultAvatarProvider(
config('features.gravatar_avatars')
? \Awcodes\FilamentGravatar\GravatarProvider::class
: \Filament\AvatarProviders\UiAvatarsProvider::class
);
Set GRAVATAR_AVATARS=false to fall back to initials instantly. Document the flag in your runbook; gravatar.com being blocked by a corporate proxy is a when, not an if.
The provider is set on the Panel, not globally, which is the main reason to use the plugin over a global binding. Pilot on the internal admin panel, leave customer-facing panels on uploaded avatars or initials:
// Admin panel — internal staff, gravatar is fine
->defaultAvatarProvider(GravatarProvider::class)
// Customer portal — privacy-sensitive, keep uploads only
->defaultAvatarProvider(UiAvatarsProvider::class)
This is also how you migrate incrementally if you later build real uploads: switch one panel at a time.
Gravatar's built-in fallbacks (mp, identicon, retro) look foreign next to a branded admin panel. Pass your own default image URL so users without a Gravatar get your placeholder rather than a mystery-person silhouette or a pixel-art robot:
// config/filament-gravatar.php (confirm key names on install)
'default' => env('GRAVATAR_DEFAULT', url('/images/avatar-default.png')),
If the config surface doesn't accept an arbitrary URL, subclass the provider and append d= yourself — the URL is just a query string.
HasAvatarFilament checks Filament\Models\Contracts\HasAvatar before consulting the avatar provider. That means users who upload a real image can bypass Gravatar without you touching the provider at all:
class User extends Authenticatable implements HasAvatar
{
public function getFilamentAvatarUrl(): ?string
{
return $this->avatar_path
? Storage::url($this->avatar_path)
: null; // null -> falls through to the panel's provider
}
}
This is the clean seam for a future "upload your own avatar" feature: ship Gravatar now, add an avatar_path column later, and the two coexist. Note the gotcha in reverse — if your User already implements HasAvatar and returns a hardcoded URL or an empty string, your new provider will appear to do nothing.
When you need behavior the config doesn't cover — skip Gravatar for service accounts, derive the email from an SSO field, log lookups — subclass the provider and swap the class name in the panel:
class AppAvatarProvider extends GravatarProvider
{
public function get(Model | HasAvatar $user): string
{
if ($user->is_service_account) {
return parent::get(...); // or a local asset
}
return parent::get($user);
}
}
Keep this as the single place avatar URLs are produced. One file, one registration point, cheap exit if you ever replace the package.
The URL is built with a size parameter, and the browser caches per-URL. Requesting 512 and letting CSS downscale avoids blurry avatars on 2x displays and avoids re-fetching when someone bumps the user-menu avatar size. Set it in config rather than threading a size through every table column.
The provider builds a string; there is no outbound PHP HTTP call. So don't reach for Http::fake() — assert the generated URL in a unit/feature test:
it('resolves a gravatar url from the user email', function () {
$user = User::factory()->make(['email' => '[email protected] ']);
expect((new GravatarProvider)->get($user))
->toContain(md5('[email protected]'));
});
Note the normalization in that assertion — it's the thing most likely to be wrong, and the cheapest thing to pin.
Once registered, these all pick up the provider automatically: Tables\Columns\ImageColumn::make('avatar') when fed the provider, the user-menu avatar, EditProfile pages, AvatarEntry infolists, and Select/Badge components that resolve avatars. Grep your codebase for defaultAvatarProvider and getFilamentAvatarUrl to find anything that bypasses the chain.
HasAvatar wins over the provider. Filament resolves getFilamentAvatarUrl() first. If avatars don't change after registering the plugin, this is the first thing to check — not the plugin.
Email normalization is the #1 cause of "wrong avatar." Gravatar hashes a trimmed, lowercased email with MD5 (SHA-256 also works). If the package trims with trim() but not mb_trim(), or skips lowercasing, users with mixed-case or whitespace-padded emails get the default image. Compare the generated URL against gravatar.com's own preview for one known account before rolling out.
v4.2.0 is a landmine; v4.2.1 closes the symptom, not the mechanism. The bug came from a Pint rule (mb_str_functions) rewriting runtime code. Two follow-ups for your own repo: audit your Pint ruleset for behavior-changing rules, and make sure CI runs --prefer-lowest on PHP 8.2 and 8.3, not just the newest interpreter. The failure is lockfile-dependent — an unrelated composer update can flip it from working to fatal with no change to this package.
No server-side HTTP call, so no latency or timeout risk. The browser fetches from secure.gravatar.com. That means: server-side tests never fail because of Gravatar, but users see broken images silently when the network blocks it. Configure a default image and consider onerror handling in a custom avatar view if the placeholder matters.
Hashed emails are still personal data. MD5 of an email is trivially reversible for known addresses. Every admin page render leaks a hashed-email lookup to a third party. This is the real blocker in regulated/EU contexts — get a documented position and a kill switch before shipping to staff, not after a works council asks.
Config changes need php artisan config:cache. Obvious, but it's the reason "I changed the default and nothing happened" tickets exist. Panels themselves aren't cached, so panel-level provider swaps take effect on the next request.
Gravatar rate-limits. Excessive requests return a 429 and Gravatar serves its default image instead of the user's. In a busy admin panel with 50-row tables this is unlikely, but it presents as "avatars randomly stopped working" — check the network tab before assuming a regression.
d=404 is your probe. If you want to know whether a user has a Gravatar (e.g., to fall back to initials rather than a generic image), request with d=404 and handle the missing image client-side. The plugin may or may not expose this — if not, subclass and append the parameter.
Extension points are deliberately thin. Config keys, the provider class, and the panel registration. There is no event, no manager, no driver system. Don't try to build a provider chain inside this package — if you need fallback ordering (SSO photo → uploaded → Gravatar → initials), implement it in a single HasAvatar method or your own provider and leave the package doing one job.
Exit cost is genuinely low — price it in. One class binding plus config. If the package stalls (single maintainer, 32 stars), copying the ~50-line provider into app/ is an afternoon. Keep the registration in a config-driven spot (§1) and the migration is a config edit, not a refactor.
How can I help you explore Laravel packages today?