mirror of
https://github.com/filamentphp/filament.git
synced 2026-10-13 09:26:05 +08:00
chore: Widen avatar providers to accept non-Eloquent users (#20707)
Refs #20623.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user