diff --git a/docs/07-users/01-overview.md b/docs/07-users/01-overview.md index 4c460d470a..60341c0578 100644 --- a/docs/07-users/01-overview.md +++ b/docs/07-users/01-overview.md @@ -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 +Custom avatar providers must accept non-Eloquent users + +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. + + The modal content is now wrapped in a `fi-modal-window-scroll` element when using a sticky header or footer diff --git a/packages/panels/src/AvatarProviders/Contracts/AvatarProvider.php b/packages/panels/src/AvatarProviders/Contracts/AvatarProvider.php index 3ebfc7ac67..54d66bd90f 100644 --- a/packages/panels/src/AvatarProviders/Contracts/AvatarProvider.php +++ b/packages/panels/src/AvatarProviders/Contracts/AvatarProvider.php @@ -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; } diff --git a/packages/upgrade/src/Rector/WidenAvatarProviderParameterTypeRector.php b/packages/upgrade/src/Rector/WidenAvatarProviderParameterTypeRector.php new file mode 100644 index 0000000000..e7f5213a6e --- /dev/null +++ b/packages/upgrade/src/Rector/WidenAvatarProviderParameterTypeRector.php @@ -0,0 +1,82 @@ +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; + } +} diff --git a/packages/upgrade/src/rector.php b/packages/upgrade/src/rector.php index e698ae881a..120dda1752 100644 --- a/packages/upgrade/src/rector.php +++ b/packages/upgrade/src/rector.php @@ -1,6 +1,7 @@ ruleWithConfiguration(RemoveTraitUseRector::class, [ 'Filament\\Schemas\\Concerns\\RestrictsFileUploadsToSchemaComponents', ]); + + $rectorConfig->rule(WidenAvatarProviderParameterTypeRector::class); }; diff --git a/tests/src/Panels/Configuration/AvatarProviderTest.php b/tests/src/Panels/Configuration/AvatarProviderTest.php index 3ec155071f..80a552d79f 100644 --- a/tests/src/Panels/Configuration/AvatarProviderTest.php +++ b/tests/src/Panels/Configuration/AvatarProviderTest.php @@ -1,8 +1,13 @@ 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']); diff --git a/tests/src/Upgrade/Fixtures/avatar_providers.php.inc b/tests/src/Upgrade/Fixtures/avatar_providers.php.inc new file mode 100644 index 0000000000..96258c9872 --- /dev/null +++ b/tests/src/Upgrade/Fixtures/avatar_providers.php.inc @@ -0,0 +1,139 @@ +doTestFile(__DIR__ . '/Fixtures/avatar_providers.php.inc'); + } + + public function provideConfigFilePath(): string + { + return dirname(__DIR__, 3) . '/packages/upgrade/src/rector.php'; + } +}