Atribut Pencarian
$globalSearchAttributes adalah daftar kolom yang boleh digunakan palet untuk mencocokkan term pencarian. Daftar ini sekaligus berfungsi sebagai mekanisme opt-in dan whitelist: resource dengan daftar kosong tidak akan dicari, dan kolom yang tidak ada di dalam daftar tidak dapat dijangkau oleh request, apa pun isi term yang dikirim. Halaman ini menjelaskan apa saja yang boleh dimasukkan ke dalam daftar tersebut dan bagaimana pencarian menggunakannya.
Contoh minimal yang dapat digunakan
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Orders;
use App\Models\Order;
use PandaPanel\Resources\Resource;
final class OrderResource extends Resource
{
protected static string $model = Order::class;
protected static ?string $recordTitleAttribute = 'reference';
/** @var list<string> */
protected static array $globalSearchAttributes = ['reference', 'customer_email'];
// ... table(), form(), pages()
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Sekarang, ketika pengguna mengetik INV-2024, pencarian akan mencocokkan order yang kolom reference atau customer_email-nya mengandung teks tersebut.
Deklarasi
/**
* @var list<string>
*/
protected static array $globalSearchAttributes = [];
public static function globalSearchAttributes(): array; // list<string>
public static function isGloballySearchable(): bool; // globalSearchAttributes() !== []2
3
4
5
6
7
Accessor dibuat public karena PandaPanel\Search\GlobalSearch membutuhkannya. Override method, bukan propertinya, jika daftar atribut bergantung pada kondisi yang baru diketahui saat request diproses:
/**
* @return list<string>
*/
public static function globalSearchAttributes(): array
{
$attributes = ['reference'];
if (auth()->user()?->is_admin === true) {
$attributes[] = 'internal_note';
}
return $attributes;
}2
3
4
5
6
7
8
9
10
11
12
13
Mengembalikan [] dari method tersebut akan menonaktifkan resource untuk pengguna itu, sama seperti jika atribut pencarian tidak pernah dideklarasikan. Nilai isGloballySearchable() diturunkan langsung dari daftar tersebut.
Bagaimana pencarian menggunakan daftar atribut
Setiap atribut menjadi satu kondisi LIKE, lalu seluruh kondisi digabungkan dengan OR di dalam satu where yang terkelompok. Term pencarian di-escape untuk LIKE sebelum pattern dibentuk:
$like = '%'.$this->escapeLike($term).'%';
$query->where(static function (Builder $query) use ($attributes, $like): void {
foreach ($attributes as $attribute) {
if (! str_contains($attribute, '.')) {
$query->orWhere($attribute, 'like', $like);
continue;
}
[$relation, $column] = explode('.', $attribute, 2);
$query->orWhereHas(
$relation,
static fn (Builder $related): Builder => $related->where($column, 'like', $like),
);
}
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Untuk ['reference', 'customer_email'] dengan term INV, SQL yang dihasilkan adalah:
select * from "orders"
where ("reference" like ? or "customer_email" like ?)
limit 52
3
Nilai %INV% di-bind sebanyak dua kali. Bentuk query ini menghasilkan dua sifat penting:
- Seluruh kondisi pencarian berada dalam satu
where, sehingga kondisi padaglobalSearchQuery()di-AND-kan dengan keseluruhan blok OR. Resource yang membatasi dirinya menggunakanwhereNotNull('published_at')tidak dapat diperluas cakupannya hanya karena sebuah term pencarian. - Term selalu menjadi bound value. Term tidak pernah digabungkan ke nama kolom maupun operator. Tidak ada nilai dari request yang masuk ke SQL sebagai identifier.
Nilai yang boleh dimasukkan ke dalam daftar
| Bentuk | Contoh | Perilaku |
|---|---|---|
| Kolom pada tabel milik resource | 'name' | where('name', 'like', '%escaped-term%') |
| Path relasi, satu level | 'author.name' | whereHas('author', fn ($q) => $q->where('name', 'like', '%escaped-term%')) — lihat Pencarian melalui relasi |
Bentuk lain yang diterima where() Eloquent sebagai kolom | 'meta->company' | diteruskan apa adanya, sehingga JSON path akan dikompilasi oleh grammar database |
Berikut yang tidak didukung:
| Tidak didukung | Alasan |
|---|---|
Accessor atau computed attribute (full_name) | Kondisi dijalankan sebagai SQL; database tidak memiliki kolom tersebut |
Qualified column (users.name) | Tanda titik selalu dianggap sebagai relasi, sehingga bentuk ini akan mencari relasi bernama users |
Path lebih dari satu level (author.company.name) | Path hanya dipecah sekali — lihat Pencarian melalui relasi |
| Kolom terenkripsi atau di-hash | LIKE tidak dapat mencocokkan ciphertext |
| Ekspresi aggregate atau subquery | Daftar hanya berisi nama kolom, bukan expression |
Daftar ini tidak divalidasi saat aplikasi melakukan boot. Salah ketik nama kolom baru akan muncul sebagai error database saat pencarian pertama kali menyentuh atribut tersebut, bukan sebagai kegagalan saat startup.
Semantik pencocokan
Operator yang digunakan adalah like dengan pattern %escaped-term% — artinya pencarian substring tanpa anchor di awal maupun akhir.
- Sensitivitas huruf besar-kecil mengikuti database, bukan framework. MySQL dengan collation
_cimelakukan pencocokan secara case-insensitive, sedangkanLIKEpada PostgreSQL tidak. Jika membutuhkan pencocokan case-insensitive di PostgreSQL, atur kolom atau collation-nya; operator pencarian tidak dapat dikonfigurasi. - Karakter
%,_, dan\pada term pengguna akan di-escape. Term%%mencari karakter persen secara literal, bukan seluruh baris yang memiliki nilai non-null. - Term di-trim dan minimal harus terdiri dari dua karakter (
mb_strlensetelahtrim). Term yang lebih pendek mengembalikan[]tanpa menjalankan query. - Tidak ada ranking. Baris yang cocok pada tiga atribut tidak otomatis ditempatkan di atas baris yang hanya cocok pada satu atribut. Tambahkan
orderBydiglobalSearchQuery()jika urutan hasil penting.
Performa
Wildcard % di awal pattern membuat index B-tree standar tidak dapat digunakan, sehingga setiap pencarian akan melakukan scan terhadap data yang tersisa setelah dibatasi oleh globalSearchQuery(). Ini masih wajar pada tabel berisi ribuan baris, tetapi tidak cocok untuk tabel berisi jutaan baris. Beberapa opsi optimasi, sesuai urutan yang biasanya paling relevan:
use Illuminate\Database\Eloquent\Builder;
// 1. Persempit kumpulan data yang dapat dicari.
public static function globalSearchQuery(): Builder
{
return static::query()->where('archived', false);
}2
3
4
5
6
7
// 2. Kurangi jumlah kolom yang dicari. Setiap atribut menambahkan kondisi OR pada setiap baris.
protected static array $globalSearchAttributes = ['reference'];2
// 3. Kurangi jatah hasil resource agar database dapat selesai lebih cepat.
protected static int $globalSearchLimit = 3;2
// 4. Naikkan debounce agar satu kata yang diketik menghasilkan satu query, bukan enam.
$panel->globalSearch(debounce: 500);2
Kolom terdenormalisasi — satu kolom teks yang selalu disinkronkan dan diberi index untuk full-text search — biasanya menjadi langkah berikutnya. Bagi $globalSearchAttributes, kolom tersebut tetap diperlakukan seperti kolom biasa. Tidak tersedia integrasi Laravel Scout sebagai fallback.
Contoh lengkap
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Customers;
use App\Models\Customer;
use Illuminate\Database\Eloquent\Builder;
use PandaPanel\Resources\Resource;
final class CustomerResource extends Resource
{
protected static string $model = Customer::class;
protected static ?string $recordTitleAttribute = 'company_name';
/**
* `search_index` adalah generated column yang menyimpan nama, email, dan telepon,
* disatukan agar pencarian hanya membutuhkan satu kondisi terindeks,
* bukan tiga kondisi yang tidak terindeks.
*
* @var list<string>
*/
protected static array $globalSearchAttributes = ['search_index'];
protected static int $globalSearchLimit = 8;
public static function globalSearchQuery(): Builder
{
return static::query()->where('status', '!=', 'archived');
}
// ... table(), form(), pages()
}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
27
28
29
30
31
32
33
34
Hal yang perlu diperhatikan
- Tanda titik selalu berarti relasi.
'orders.reference'akan menjadiwhereHaspada relasiorders, bukan qualified column. JikaglobalSearchQuery()melakukan join dan nama kolom menjadi ambigu, masalah tersebut tidak dapat diperbaiki dari daftar ini — ubah nama kolom pada query atau gunakan alias yang sesuai. - Daftar kosong berarti resource keluar dari pencarian secara diam-diam. Menghapus atribut terakhir saat refactor akan menghilangkan resource dari palet. Jika resource tersebut adalah satu-satunya resource yang dapat dicari, palet juga akan menghilang dari header.
- Salah ketik nama kolom gagal saat query dijalankan. Di antarmuka, kondisi ini dapat terlihat seperti pencarian yang menghasilkan "Nothing found." Periksa log aplikasi, bukan hanya dialog pencarian.
- Nilai
nulltidak pernah cocok.LIKEterhadapNULLmenghasilkan unknown, sehingga baris dengan kolom kosong tidak dianggap sebagai hasil. - Kolom numerik dapat digunakan, tetapi biasanya hasilnya tidak seperti yang diinginkan.
where('id', 'like', '%12%')dapat mencocokkan 12, 120, dan 512. Untuk identifier yang diketik pengguna, lebih baik cari melalui reference string daripada primary key. - Daftar dibaca pada setiap pencarian. Jika Anda meng-override
globalSearchAttributes()dengan proses yang mahal, biaya tersebut akan terjadi pada setiap input setelah debounce.