Pencarian melalui Relasi
Atribut yang mengandung tanda titik akan mencari pada relasi yang disebutkan, bukan pada kolom di tabel milik resource itu sendiri. Dengan cara ini, sebuah post dapat ditemukan berdasarkan nama author-nya, atau sebuah order berdasarkan email customer-nya, tanpa perlu melakukan denormalisasi data. Halaman ini membahas sintaks relasi yang didukung, dengan kedalaman tepat satu level, serta pilihan yang dapat digunakan jika Anda membutuhkan relasi yang lebih dalam.
Contoh minimal yang dapat digunakan
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Posts;
use App\Models\Post;
use PandaPanel\Resources\Resource;
final class PostResource extends Resource
{
protected static string $model = Post::class;
protected static ?string $recordTitleAttribute = 'title';
/** @var list<string> */
protected static array $globalSearchAttributes = ['title', 'author.name', 'author.email'];
// ... table(), form(), pages()
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Post::author() adalah relasi Eloquent biasa. Setelah konfigurasi di atas, mengetik Lovelace akan menemukan setiap post yang judulnya mengandung teks tersebut atau yang nama maupun email author-nya cocok.
Sintaks
{relation}.{column}Bagian sebelum tanda titik pertama adalah method relasi pada model milik resource. Seluruh bagian setelah tanda titik digunakan sebagai nama kolom pada tabel terkait. $like adalah pattern %escaped-term% yang sudah di-escape dan disiapkan oleh query builder pencarian:
[$relation, $column] = explode('.', $attribute, 2);
$query->orWhereHas(
$relation,
static fn (Builder $related): Builder => $related->where($column, 'like', $like),
);2
3
4
5
6
Jadi ['title', 'author.name'] dengan term Ada akan dikompilasi menjadi:
select * from "posts"
where (
"title" like ?
or exists (
select * from "users"
where "posts"."author_id" = "users"."id" and "name" like ?
)
)
limit 52
3
4
5
6
7
8
9
Setiap atribut bertanda titik menghasilkan satu subquery EXISTS, lalu di-OR-kan dengan kondisi lainnya di dalam grup where yang sama dengan kolom biasa.
Relasi yang didukung
Semua jenis relasi yang dapat diterima whereHas():
| Relasi | Didukung | Catatan |
|---|---|---|
belongsTo | ya | kasus paling umum — misalnya author dari sebuah post |
hasOne / hasMany | ya | cocok ketika salah satu record terkait memenuhi kondisi |
belongsToMany | ya | join ke pivot dibangun oleh relasi |
hasOneThrough / hasManyThrough | ya | dapat digunakan untuk menjangkau dua level — lihat bagian berikutnya |
morphOne / morphMany / morphToMany | ya | constraint morph type berasal dari definisi relasi |
morphTo | tidak | Eloquent tidak dapat membatasi polymorphic parent menggunakan whereHas; pencarian tidak menyediakan padanan whereHasMorph |
Constraint milik relasi tetap ikut diterapkan: misalnya hasMany yang sudah difilter dalam definisinya, model terkait yang menggunakan SoftDeletes, atau global scope pada model terkait. Author yang sudah di-trash tidak akan cocok karena subquery dibangun dari definisi relasi, bukan dari nama tabel yang ditebak framework.
Hanya satu level
Path hanya dipecah sekali. author.company.name akan dibaca sebagai relasi author dan kolom company.name. Kolom yang mengandung tanda titik tersebut kemudian dikompilasi menjadi "company"."name", yaitu referensi ke tabel yang tidak ada di dalam subquery. Database akan menolaknya. Tidak ada peringatan saat boot; error baru muncul ketika pencarian pertama kali menyentuh atribut tersebut.
Ada tiga solusi yang biasanya dapat dipilih, sesuai urutan yang umum digunakan.
Definisikan through-relation pada model agar dua lompatan relasi menjadi satu:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
final class Post extends Model
{
public function authorCompany(): HasOneThrough
{
return $this->hasOneThrough(Company::class, User::class, 'id', 'id', 'author_id', 'company_id');
}
}2
3
4
5
6
7
8
9
10
/** @var list<string> */
protected static array $globalSearchAttributes = ['title', 'authorCompany.name'];2
Denormalisasikan nilai ke tabel milik resource. Ini merupakan pilihan yang tepat jika pencarian sering digunakan dan nilainya jarang berubah:
/** @var list<string> */
protected static array $globalSearchAttributes = ['title', 'author_name'];2
Persempit desain pencarian, bukan memperdalam path. Jika pengguna benar-benar perlu mencari post berdasarkan company, sering kali antarmuka yang lebih baik adalah menyediakan Company resource yang hasil pencariannya kemudian mengarahkan pengguna ke data terkait.
Menggabungkan dengan eager loading
Mencari melalui relasi dan menampilkan data relasi adalah dua hal berbeda. whereHas melakukan filter tanpa melakukan loading relasi. Jika globalSearchResultDetails() membaca relasi tersebut, lakukan eager loading; jika tidak, Anda akan menghasilkan satu query tambahan untuk setiap hasil:
use Illuminate\Database\Eloquent\Model;
final class PostResource extends Resource
{
/** @var list<string> */
protected static array $globalSearchAttributes = ['title', 'author.name'];
/**
* `query()` menerapkan eager load ini, dan `globalSearchQuery()` dimulai
* dari `query()`, sehingga pencarian mendapat eager load tanpa perlu
* mendeklarasikannya kembali.
*
* @var list<string>
*/
protected static array $with = ['author'];
/**
* @return array<string, string>
*/
public static function globalSearchResultDetails(Model $record): array
{
$author = $record->getAttribute('author');
return ['Author' => $author instanceof Model ? (string) $author->getAttribute('name') : '—'];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Model::shouldBeStrict() aktif di luar production pada starter kit, sehingga eager load yang terlupakan akan gagal secara eksplisit alih-alih diam-diam menghasilkan satu query tambahan per row.
Mempersempit pencarian lebih lanjut
globalSearchQuery() juga dapat membatasi relasi. Kondisinya akan di-AND-kan dengan keseluruhan blok pencarian:
use Illuminate\Database\Eloquent\Builder;
public static function globalSearchQuery(): Builder
{
return static::query()
->with('author')
->whereHas('author', static fn (Builder $author): Builder => $author->where('active', true));
}2
3
4
5
6
7
8
Maknanya adalah "post milik author yang aktif, lalu term boleh cocok di atribut pencarian mana pun" — bukan "post yang author-nya aktif atau judulnya cocok".
Hal yang perlu diperhatikan
- Tanda titik selalu berarti relasi. Tidak ada cara menuliskan qualified column pada
$globalSearchAttributes;users.namedianggap mencari relasi bernamausers. - Relasi yang tidak ada menghasilkan runtime error.
whereHas('athor', …)akan melemparBadMethodCallExceptionsaat pencarian pertama, bukan saat boot. morphTotidak dapat dicari. Cari concrete resource secara terpisah dan biarkan masing-masing menyumbangkan grup hasilnya sendiri.- Setiap atribut bertanda titik menghasilkan
EXISTSterpisah. Tiga atribut relasi berarti tiga subquery per pencarian, masing-masing menggunakanLIKEdengan wildcard di awal. Gunakan index sejauh memungkinkan dan pertimbangkan satu kolom terdenormalisasi ketika palet mulai terasa lambat. whereHasmencocokkan row, bukan value. AtributhasManymembuat parent dianggap cocok ketika salah satu child cocok; child mana yang cocok tidak otomatis ditampilkan. Tambahkan informasi identifikasi padaglobalSearchResultDetails()jika tanpa informasi tersebut pengguna dapat bingung.- Global scope pada model terkait tetap berlaku. Biasanya ini memang diinginkan, misalnya agar author yang sudah dihapus tidak muncul, tetapi scoped relation juga dapat membuat record tidak dapat ditemukan meskipun term-nya secara fisik ada di database.