chore: Widen avatar providers to accept non-Eloquent users (#20707)

Refs #20623.
This commit is contained in:
Dan Harrin
2026-10-11 20:18:57 +01:00
committed by GitHub
parent d958f10998
commit 6fd1afd55c
8 changed files with 310 additions and 2 deletions
+1 -1
View File
@@ -95,7 +95,7 @@ The `getFilamentAvatarUrl()` method is used to retrieve the avatar of the curren
You can easily swap out [ui-avatars.com](https://ui-avatars.com) for a different service, by creating a new avatar provider.
In this example, we create a new file at `app/Filament/AvatarProviders/BoringAvatarsProvider.php` for [boringavatars.com](https://boringavatars.com). The `get()` method accepts a user model instance and returns an avatar URL for that user:
In this example, we create a new file at `app/Filament/AvatarProviders/BoringAvatarsProvider.php` for [boringavatars.com](https://boringavatars.com). The `get()` method accepts an Eloquent model or an `Authenticatable` user instance and returns an avatar URL for that record:
```php
<?php
+24
View File
@@ -346,6 +346,30 @@ See the [security documentation](../advanced/security#restricting-livewire-file-
### Low-impact changes
<Disclosure open x-show="packages.includes('panels')">
<span slot="summary">Custom avatar providers must accept non-Eloquent users</span>
The `AvatarProvider::get()` method now accepts `Model | Authenticatable` instead of only `Model`, so avatar providers can receive users that do not extend an Eloquent model.
If you have a custom avatar provider, update its `get()` parameter type:
```php
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
public function get(Model | Authenticatable $record): string
{
// Return the avatar URL for the record.
}
```
The automated upgrade script updates this parameter type when `get()` is declared directly on a class implementing `AvatarProvider` or an interface extending it. If your provider uses a `get()` method declared in a trait or inherited from a parent class that does not implement `AvatarProvider`, you must update that declaration manually.
You should review your provider's implementation for Eloquent-specific method calls and handle non-Eloquent users if your application uses them. Providers that already accept both types, `object`, `mixed`, or an untyped parameter do not need a signature change.
See the [custom avatar provider documentation](../users/overview#using-a-different-avatar-provider) for more details.
</Disclosure>
<Disclosure open x-show="packages.includes('support')">
<span slot="summary">The modal content is now wrapped in a `fi-modal-window-scroll` element when using a sticky header or footer</span>
@@ -2,9 +2,10 @@
namespace Filament\AvatarProviders\Contracts;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
interface AvatarProvider
{
public function get(Model $record): string;
public function get(Model | Authenticatable $record): string;
}
@@ -0,0 +1,82 @@
<?php
namespace Filament\Upgrade\Rector;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Interface_;
use PHPStan\Type\ObjectType;
use PHPStan\Type\TypeCombinator;
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
use Rector\Rector\AbstractRector;
use Rector\StaticTypeMapper\StaticTypeMapper;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
class WidenAvatarProviderParameterTypeRector extends AbstractRector
{
public function __construct(
private StaticTypeMapper $staticTypeMapper,
) {}
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Allow avatar providers to accept non-Eloquent users', [
new CodeSample(
<<<'PHP'
class CustomAvatarProvider implements \Filament\AvatarProviders\Contracts\AvatarProvider
{
public function get(\Illuminate\Database\Eloquent\Model $record): string
{
return 'https://example.com/avatar.svg';
}
}
PHP,
<<<'PHP'
class CustomAvatarProvider implements \Filament\AvatarProviders\Contracts\AvatarProvider
{
public function get(\Illuminate\Database\Eloquent\Model | \Illuminate\Contracts\Auth\Authenticatable $record): string
{
return 'https://example.com/avatar.svg';
}
}
PHP,
),
]);
}
public function getNodeTypes(): array
{
return [Class_::class, Interface_::class];
}
/**
* @param Class_ | Interface_ $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->isObjectType($node, new ObjectType('Filament\\AvatarProviders\\Contracts\\AvatarProvider'))) {
return null;
}
$parameter = $node->getMethod('get')?->params[0] ?? null;
if (! $parameter?->type) {
return null;
}
$parameterType = $this->getType($parameter->type);
$authenticatableType = new ObjectType('Illuminate\\Contracts\\Auth\\Authenticatable');
if ($parameterType->isSuperTypeOf($authenticatableType)->yes()) {
return null;
}
$parameter->type = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode(
TypeCombinator::union($parameterType, $authenticatableType),
TypeKind::PARAM,
);
return $node;
}
}
+3
View File
@@ -1,6 +1,7 @@
<?php
use Composer\InstalledVersions;
use Filament\Upgrade\Rector\WidenAvatarProviderParameterTypeRector;
use Rector\Config\RectorConfig;
use Rector\Removing\Rector\Class_\RemoveTraitUseRector;
@@ -12,4 +13,6 @@ return static function (RectorConfig $rectorConfig): void {
$rectorConfig->ruleWithConfiguration(RemoveTraitUseRector::class, [
'Filament\\Schemas\\Concerns\\RestrictsFileUploadsToSchemaComponents',
]);
$rectorConfig->rule(WidenAvatarProviderParameterTypeRector::class);
};
@@ -1,8 +1,13 @@
<?php
use Filament\AvatarProviders\UiAvatarsProvider;
use Filament\Facades\Filament;
use Filament\Models\Contracts\HasAvatar;
use Filament\Models\Contracts\HasName;
use Filament\Tests\Fixtures\Models\User;
use Filament\Tests\Panels\Configuration\TestCase;
use Illuminate\Auth\GenericUser;
use Illuminate\Database\Eloquent\Model;
uses(TestCase::class);
@@ -16,6 +21,42 @@ it('returns a URL string from `get()`', function (): void {
expect($url)->toStartWith('https://ui-avatars.com/api/');
});
it('generates default avatars for `Model` and non-Eloquent `Authenticatable` records', function (bool $isEloquent): void {
$record = $isEloquent
? new class extends Model implements HasAvatar, HasName
{
public function getFilamentAvatarUrl(): ?string
{
return null;
}
public function getFilamentName(): string
{
return 'Taylor Morgan';
}
}
: new class([]) extends GenericUser implements HasAvatar, HasName
{
public function getFilamentAvatarUrl(): ?string
{
return null;
}
public function getFilamentName(): string
{
return 'Jordan Ellis';
}
};
$url = Filament::getUserAvatarUrl($record);
expect($url)->toStartWith('https://ui-avatars.com/api/');
expect($url)->toContain($isEloquent ? 'name=T+M' : 'name=J+E');
})->with([
'Eloquent model' => [true],
'non-Eloquent user' => [false],
]);
it('includes encoded name in the URL', function (): void {
$provider = new UiAvatarsProvider;
$user = User::factory()->create(['name' => 'Jane Smith']);
@@ -0,0 +1,139 @@
<?php
namespace Filament\Tests\Upgrade\Fixtures\AvatarProviders;
use Filament\AvatarProviders\Contracts\AvatarProvider as Provider;
use Filament\AvatarProviders\UiAvatarsProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model as Record;
class CustomAvatarProvider implements Provider
{
public function get(Record $user): string
{
return 'https://example.com/avatar.svg';
}
}
interface CustomAvatarProviderContract extends Provider
{
public function get(Record $record): string;
}
class InheritedAvatarProvider extends UiAvatarsProvider
{
public function get(?Record $record): string
{
return 'https://example.com/avatar.svg';
}
}
class UnionAvatarProvider implements Provider
{
public function get(Record | Authenticatable $record): string
{
return 'https://example.com/avatar.svg';
}
}
class MixedAvatarProvider implements Provider
{
public function get(mixed $record): string
{
return 'https://example.com/avatar.svg';
}
}
class ObjectAvatarProvider implements Provider
{
public function get(object $record): string
{
return 'https://example.com/avatar.svg';
}
}
class UntypedAvatarProvider implements Provider
{
public function get($record): string
{
return 'https://example.com/avatar.svg';
}
}
class UnrelatedProvider
{
public function get(Record $record): string
{
return 'https://example.com/avatar.svg';
}
}
-----
<?php
namespace Filament\Tests\Upgrade\Fixtures\AvatarProviders;
use Filament\AvatarProviders\Contracts\AvatarProvider as Provider;
use Filament\AvatarProviders\UiAvatarsProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model as Record;
class CustomAvatarProvider implements Provider
{
public function get(\Illuminate\Database\Eloquent\Model|\Illuminate\Contracts\Auth\Authenticatable $user): string
{
return 'https://example.com/avatar.svg';
}
}
interface CustomAvatarProviderContract extends Provider
{
public function get(\Illuminate\Database\Eloquent\Model|\Illuminate\Contracts\Auth\Authenticatable $record): string;
}
class InheritedAvatarProvider extends UiAvatarsProvider
{
public function get(\Illuminate\Database\Eloquent\Model|\Illuminate\Contracts\Auth\Authenticatable|null $record): string
{
return 'https://example.com/avatar.svg';
}
}
class UnionAvatarProvider implements Provider
{
public function get(Record | Authenticatable $record): string
{
return 'https://example.com/avatar.svg';
}
}
class MixedAvatarProvider implements Provider
{
public function get(mixed $record): string
{
return 'https://example.com/avatar.svg';
}
}
class ObjectAvatarProvider implements Provider
{
public function get(object $record): string
{
return 'https://example.com/avatar.svg';
}
}
class UntypedAvatarProvider implements Provider
{
public function get($record): string
{
return 'https://example.com/avatar.svg';
}
}
class UnrelatedProvider
{
public function get(Record $record): string
{
return 'https://example.com/avatar.svg';
}
}
@@ -0,0 +1,18 @@
<?php
namespace Filament\Tests\Upgrade;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
class WidenAvatarProviderParameterTypeRectorTest extends AbstractRectorTestCase
{
public function test_widens_avatar_providers_without_narrowing_compatible_signatures(): void
{
$this->doTestFile(__DIR__ . '/Fixtures/avatar_providers.php.inc');
}
public function provideConfigFilePath(): string
{
return dirname(__DIR__, 3) . '/packages/upgrade/src/rector.php';
}
}