Abilitare la ricerca globale in Laravel Filament
La ricerca globale di Filament permette agli utenti del pannello di amministrazione di cercare record tra risorse diverse attraverso un unico campo di input accessibile dalla barra di navigazione superiore. Si tratta di una funzionalità particolarmente utile in applicazioni con molte risorse, dove navigare tra le sezioni per trovare un record specifico risulterebbe inefficiente. In questo articolo vedremo come configurarla in modo completo, dalle impostazioni di base alle personalizzazioni avanzate.
Requisiti e struttura del progetto
Si assume di lavorare con Laravel 10 o superiore e Filament v3, installato tramite Composer e configurato con un pannello di amministrazione. La struttura di una risorsa Filament standard prevede una classe che estende Resource e definisce il modello Eloquent associato, il form, la tabella e le pagine disponibili.
Per abilitare la ricerca globale su una risorsa è sufficiente intervenire sulla classe risorsa stessa: non occorre modificare il pannello o i provider di configurazione. Ogni risorsa gestisce autonomamente la propria partecipazione alla ricerca globale.
Abilitare la ricerca globale su una risorsa
Il primo passo è indicare a Filament quali attributi del modello devono essere usati per la ricerca. Questo si fa sovrascrivendo il metodo statico getGloballySearchableAttributes() nella classe risorsa. Il metodo restituisce un array di nomi di colonne del database o di relazioni tramite dot notation.
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\UserResource\Pages;
use App\Models\User;
use Filament\Forms;
use Filament\Resources\Resource;
use Filament\Tables;
class UserResource extends Resource
{
// Modello Eloquent associato alla risorsa
protected static ?string $model = User::class;
// Icona mostrata nella navigazione laterale
protected static ?string $navigationIcon = 'heroicon-o-users';
// Attributi del modello usati per la ricerca globale
public static function getGloballySearchableAttributes(): array
{
return ['name', 'email', 'phone'];
}
// Definizione del form di creazione e modifica
public static function form(Forms\Form $form): Forms\Form
{
return $form->schema([
Forms\Components\TextInput::make('name')->required(),
Forms\Components\TextInput::make('email')->email()->required(),
Forms\Components\TextInput::make('phone'),
]);
}
// Definizione della tabella elenco
public static function table(Tables\Table $table): Tables\Table
{
return $table->columns([
Tables\Columns\TextColumn::make('name'),
Tables\Columns\TextColumn::make('email'),
]);
}
// Pagine registrate per questa risorsa
public static function getPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'),
];
}
}
Con questa configurazione minima, digitando un testo nel campo di ricerca globale del pannello, Filament interrogherà la tabella degli utenti cercando corrispondenze su name, email e phone. I risultati vengono mostrati in un menu a tendina organizzato per tipo di risorsa.
Ricerca su attributi di relazioni
Filament supporta la ricerca su attributi di relazioni tramite dot notation. Ad esempio, se il modello User appartiene a un Company tramite una relazione company, è possibile includere il nome dell'azienda nei criteri di ricerca.
<?php
// Attributi di ricerca che includono una relazione belongsTo
public static function getGloballySearchableAttributes(): array
{
return [
'name',
'email',
'company.name', // Cerca anche sul nome dell'azienda collegata
];
}
Internamente, Filament utilizza la funzione whereHas di Eloquent per gestire la ricerca sulle relazioni. È importante che la relazione sia definita correttamente nel modello, altrimenti la query genererà un'eccezione.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class User extends Model
{
protected $fillable = ['name', 'email', 'phone', 'company_id'];
// Relazione con il modello Company
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
}
Personalizzare i risultati visualizzati
Per impostazione predefinita, Filament mostra il valore dell'attributo del titolo del modello in ogni risultato di ricerca. È possibile controllare quali informazioni vengono visualizzate sovrascrivendo il metodo getGlobalSearchResultTitle(), che riceve il record corrente e deve restituire una stringa o un oggetto Htmlable.
<?php
use Illuminate\Database\Eloquent\Model;
// Titolo personalizzato mostrato nel risultato di ricerca globale
public static function getGlobalSearchResultTitle(Model $record): string
{
// Mostra nome e indirizzo email separati da un trattino
return $record->name . ' — ' . $record->email;
}
Oltre al titolo è possibile aggiungere dettagli descrittivi sotto di esso, utili per distinguere record con nomi simili. Il metodo getGlobalSearchResultDetails() restituisce un array associativo in cui le chiavi sono le etichette e i valori sono i contenuti.
<?php
use Illuminate\Database\Eloquent\Model;
// Dettagli aggiuntivi mostrati sotto il titolo nel pannello dei risultati
public static function getGlobalSearchResultDetails(Model $record): array
{
return [
'Azienda' => $record->company?->name ?? 'Nessuna',
'Telefono' => $record->phone ?? 'Non disponibile',
'Ruolo' => ucfirst($record->role),
];
}
Personalizzare il link del risultato
Quando un utente clicca su un risultato di ricerca, Filament lo indirizza alla pagina di modifica del record. Questo comportamento predefinito può essere modificato sovrascrivendo getGlobalSearchResultUrl(), che riceve il record e deve restituire una stringa con l'URL di destinazione.
<?php
use Illuminate\Database\Eloquent\Model;
// URL di destinazione quando si clicca su un risultato di ricerca
public static function getGlobalSearchResultUrl(Model $record): string
{
// Reindirizza alla pagina di visualizzazione invece che alla modifica
return static::getUrl('view', ['record' => $record]);
}
Se la risorsa non ha una pagina view registrata, è necessario aggiungerla prima nell'array restituito da getPages().
<?php
// Aggiunta della pagina di visualizzazione alle pagine della risorsa
public static function getPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'),
'view' => Pages\ViewUser::route('/{record}'),
'edit' => Pages\EditUser::route('/{record}/edit'),
];
}
Aggiungere azioni ai risultati
Filament consente di associare azioni rapide a ciascun risultato di ricerca globale, rendendolo cliccabile con operazioni contestuali. Il metodo getGlobalSearchResultActions() restituisce un array di oggetti Action.
<?php
use Filament\GlobalSearch\Actions\Action;
use Illuminate\Database\Eloquent\Model;
// Azioni mostrate accanto a ogni risultato nella ricerca globale
public static function getGlobalSearchResultActions(Model $record): array
{
return [
// Azione per modificare direttamente il record dal risultato
Action::make('edit')
->label('Modifica')
->icon('heroicon-m-pencil-square')
->url(static::getUrl('edit', ['record' => $record])),
// Azione per visualizzare il profilo dell'utente
Action::make('view')
->label('Visualizza')
->icon('heroicon-m-eye')
->url(static::getUrl('view', ['record' => $record])),
];
}
Limitare i risultati restituiti
Il numero massimo di risultati mostrati per risorsa nella ricerca globale è controllato dalla proprietà statica $globalSearchResultsLimit. Il valore predefinito è 50, ma può essere ridotto per migliorare le prestazioni o per ragioni di usabilità.
<?php
class UserResource extends Resource
{
// Numero massimo di risultati restituiti dalla ricerca globale per questa risorsa
protected static int $globalSearchResultsLimit = 10;
// ... resto della classe
}
Modificare la query di ricerca globale
In scenari avanzati potrebbe essere necessario modificare la query Eloquent utilizzata dalla ricerca globale, ad esempio per aggiungere clausole di eager loading, filtrare record per stato o limitare i risultati in base a condizioni applicative. Il metodo da sovrascrivere è modifyGlobalSearchQuery().
<?php
use Illuminate\Database\Eloquent\Builder;
// Modifica della query usata dalla ricerca globale
public static function modifyGlobalSearchQuery(Builder $query, string $search): Builder
{
return $query
// Carica in anticipo la relazione company per evitare query N+1
->with('company')
// Esclude gli utenti sospesi dai risultati di ricerca
->where('status', '!=', 'suspended');
}
Il metodo riceve il builder già filtrato da Filament con le clausole where sugli attributi cercabili, quindi è sufficiente concatenare le condizioni aggiuntive senza sovrascrivere quelle esistenti.
Disabilitare la ricerca globale su una risorsa
Se non si vuole che una risorsa compaia nei risultati della ricerca globale, è sufficiente non definire il metodo getGloballySearchableAttributes() oppure restituire un array vuoto. In alternativa, si può impostare la proprietà $globalSearchable a false.
<?php
class LogResource extends Resource
{
// Questa risorsa non deve comparire nella ricerca globale
protected static bool $globalSearchable = false;
// ... resto della classe
}
Configurare il pannello per la ricerca globale
La ricerca globale viene abilitata automaticamente non appena almeno una risorsa definisce attributi cercabili. Tuttavia, dal provider del pannello è possibile configurare alcune opzioni globali come il limite di risultati per ciascun gruppo e il debounce del campo di input.
<?php
namespace App\Providers\Filament;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->colors(['primary' => Color::Amber])
// Abilita la ricerca globale con debounce di 500 millisecondi
->globalSearch()
// Imposta il numero di risultati massimi mostrati per gruppo di risorsa
->globalSearchResultsLimit(8)
->discoverResources(
in: app_path('Filament/Resources'),
for: 'App\\Filament\\Resources'
)
->pages([])
->middleware([])
->authMiddleware([]);
}
}
Scorciatoia da tastiera
Filament espone una scorciatoia da tastiera per aprire il campo di ricerca globale senza usare il mouse. La combinazione predefinita dipende dal sistema operativo: su macOS è Cmd+K, su Windows e Linux è Ctrl+K. Non sono richieste configurazioni aggiuntive per abilitarla.
Se si vuole documentare la scorciatoia nell'interfaccia, è possibile aggiungere un tooltip personalizzato tramite una view Blade o una stringa localizzata nel file di lingua di Filament. I file di lingua della libreria si trovano in lang/vendor/filament dopo la pubblicazione tramite il comando Artisan.
# Pubblica i file di lingua di Filament per personalizzarli
php artisan vendor:publish --tag=filament-translations
Esempio completo con due risorse
Per consolidare quanto visto, di seguito viene presentato un esempio con due risorse complete: UserResource e PostResource. Entrambe partecipano alla ricerca globale con configurazioni diverse, mostrando come ciascuna risorsa possa personalizzare il proprio comportamento in modo indipendente.
<?php
namespace App\Filament\Resources;
use App\Models\User;
use Filament\GlobalSearch\Actions\Action;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static ?string $navigationIcon = 'heroicon-o-users';
// Campi su cui viene eseguita la ricerca globale
public static function getGloballySearchableAttributes(): array
{
return ['name', 'email', 'company.name'];
}
// Titolo del risultato: nome e ruolo separati da virgola
public static function getGlobalSearchResultTitle(Model $record): string
{
return $record->name . ', ' . ucfirst($record->role);
}
// Dettagli secondari mostrati sotto il titolo
public static function getGlobalSearchResultDetails(Model $record): array
{
return [
'Email' => $record->email,
'Azienda' => $record->company?->name ?? 'Nessuna',
];
}
// Eager loading della relazione company per evitare query N+1
public static function modifyGlobalSearchQuery(Builder $query, string $search): Builder
{
return $query->with('company');
}
// Azioni disponibili direttamente dal risultato di ricerca
public static function getGlobalSearchResultActions(Model $record): array
{
return [
Action::make('edit')
->label('Modifica')
->icon('heroicon-m-pencil-square')
->url(static::getUrl('edit', ['record' => $record])),
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUsers::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'),
];
}
}
<?php
namespace App\Filament\Resources;
use App\Models\Post;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class PostResource extends Resource
{
protected static ?string $model = Post::class;
protected static ?string $navigationIcon = 'heroicon-o-document-text';
// Cerca su titolo, sommario e nome dell'autore tramite relazione
public static function getGloballySearchableAttributes(): array
{
return ['title', 'excerpt', 'author.name'];
}
// Mostra il titolo del post come intestazione del risultato
public static function getGlobalSearchResultTitle(Model $record): string
{
return $record->title;
}
// Informazioni aggiuntive: autore e data di pubblicazione
public static function getGlobalSearchResultDetails(Model $record): array
{
return [
'Autore' => $record->author?->name ?? 'Sconosciuto',
'Pubblicato' => $record->published_at?->format('d/m/Y') ?? 'Bozza',
'Categoria' => $record->category?->name ?? 'Nessuna',
];
}
// Limita la ricerca ai soli post pubblicati
public static function modifyGlobalSearchQuery(Builder $query, string $search): Builder
{
return $query
->with(['author', 'category'])
->whereNotNull('published_at');
}
// Numero massimo di post mostrati nei risultati di ricerca
protected static int $globalSearchResultsLimit = 5;
public static function getPages(): array
{
return [
'index' => Pages\ListPosts::route('/'),
'create' => Pages\CreatePost::route('/create'),
'edit' => Pages\EditPost::route('/{record}/edit'),
];
}
}
Considerazioni sulle prestazioni
La ricerca globale esegue una query separata per ogni risorsa che espone attributi cercabili. In applicazioni con molte risorse e tabelle di grandi dimensioni, questo può comportare un numero elevato di query simultanee. Per mantenere prestazioni accettabili è consigliabile seguire alcune buone pratiche.
Prima di tutto, assicurarsi che le colonne indicate in getGloballySearchableAttributes() siano indicizzate nel database. Una ricerca con LIKE '%termine%' su colonne non indicizzate è particolarmente costosa su tabelle con milioni di righe. Per colonne testuali di grandi dimensioni, considerare l'uso di indici full-text supportati da MySQL o PostgreSQL.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
// Aggiunge un indice full-text sulle colonne usate dalla ricerca globale
$table->fullText(['title', 'excerpt']);
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropFullText(['title', 'excerpt']);
});
}
};
In secondo luogo, usare sempre modifyGlobalSearchQuery() per caricare le relazioni con with() invece di affidarsi al caricamento lazy, che produrrebbe query aggiuntive per ogni record restituito. Infine, impostare un valore ragionevole per $globalSearchResultsLimit: restituire cinquanta record con dettagli di relazioni caricate in lazy loading è significativamente più costoso che restituirne dieci con eager loading.
Conclusioni
La ricerca globale di Filament è un meccanismo flessibile e ben integrato nel ciclo di vita delle risorse. La sua configurazione richiede interventi minimi sul codice esistente: basta definire gli attributi cercabili e, se necessario, personalizzare titolo, dettagli e azioni dei risultati. Per applicazioni in produzione con volumi di dati significativi, l'attenzione alle prestazioni tramite indici e eager loading è fondamentale per garantire un'esperienza di ricerca fluida.