Filter Query Builder
QueryBuilderFilter memungkinkan pengguna menyusun condition sendiri: sebuah daftar rule datar, dan setiap rule menyebut kolom, operator, serta value. Gunakan ketika Anda tidak dapat memprediksi pertanyaan apa yang ingin diajukan pengguna terhadap tabel — misalnya pada support view, report, atau audit log. Jika bentuk pertanyaannya sudah diketahui, SelectFilter atau FormFilter biasanya merupakan pilihan yang lebih baik.
Contoh minimal
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Users\Tables;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Filters\Constraints\TextConstraint;
use PandaPanel\Tables\Filters\QueryBuilderFilter;
use PandaPanel\Tables\TableSchema;
final class UsersTable
{
public static function configure(TableSchema $table): TableSchema
{
return $table
->columns([
TextColumn::make('name'),
TextColumn::make('email'),
])
->filters([
QueryBuilderFilter::make('conditions')
->label('Advanced')
->constraints([
TextConstraint::make('name'),
TextConstraint::make('email'),
]),
]);
}
}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
Tabel sekarang memiliki filter "Advanced" tempat pengguna dapat menambahkan rule seperti Name contains ada. Semua rule digabung menggunakan AND dan diterapkan di dalam group miliknya sendiri, sehingga rule mempersempit hasil yang sudah tersisa setelah search dan filter lainnya.
Cara sebuah rule divalidasi
Setiap bagian dari rule yang dikirim diperiksa terhadap declaration sebelum mencapai builder. Tidak ada nilai request yang digabungkan langsung ke query: nama kolom berasal dari objek Constraint, sedangkan comparison berasal dari enum tertutup.
| Bagian rule | Diperiksa terhadap | Jika gagal |
|---|---|---|
column | QueryBuilderFilter::constraint($name) | rule dibuang |
operator | ConstraintOperator::tryFrom(), lalu Constraint::supports() | rule dibuang |
value | Constraint::accepts($operator, $value) | rule dibuang |
Rule yang gagal salah satu pemeriksaan tersebut dibuang, bukan diperbaiki. Query yang tidak pernah diminta pengguna lebih buruk daripada tidak menerapkan rule sama sekali. Jika semua rule dibuang, sanitize() mengembalikan null, filter tidak menerapkan apa pun, dan statusnya dianggap tidak aktif. Frontend tidak akan menampilkan chip untuk condition yang sebenarnya diabaikan query.
QueryBuilderFilter
use PandaPanel\Tables\Enums\FilterType;
use PandaPanel\Tables\Filters\Constraints\Constraint;
use PandaPanel\Tables\Filters\QueryBuilderFilter;
QueryBuilderFilter::make(string $name): static
QueryBuilderFilter::constraints(array $constraints): self // array<array-key, Constraint>
QueryBuilderFilter::maxRules(int $max): self // default 10, floored at 1
QueryBuilderFilter::constraint(string $name): ?Constraint
QueryBuilderFilter::sanitize(mixed $value): ?array
QueryBuilderFilter::type(): FilterType // FilterType::QueryBuilder2
3
4
5
6
7
8
9
10
use PandaPanel\Tables\Filters\Constraints\BooleanConstraint;
use PandaPanel\Tables\Filters\Constraints\DateConstraint;
use PandaPanel\Tables\Filters\Constraints\NumberConstraint;
use PandaPanel\Tables\Filters\Constraints\TextConstraint;
use PandaPanel\Tables\Filters\QueryBuilderFilter;
QueryBuilderFilter::make('conditions')
->label('Advanced')
->maxRules(5)
->constraints([
TextConstraint::make('name'),
TextConstraint::make('email'),
NumberConstraint::make('login_count')->label('Sign-ins'),
DateConstraint::make('created_at')->label('Registered'),
BooleanConstraint::make('is_admin')->label('Administrator'),
]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
maxRules() membatasi jumlah condition pada satu filter agar tidak berubah menjadi daftar rule tanpa batas. Rule setelah batas dipotong saat parsing; frontend juga berhenti menawarkan "add condition" ketika jumlah rule mencapai batas.
Semua kemampuan base Filter diwarisi dan tetap bekerja:
use Illuminate\Database\Eloquent\Builder;
QueryBuilderFilter::make('conditions')
->label('Advanced')
->constraints([TextConstraint::make('name')])
->default([['column' => 'name', 'operator' => 'is_filled']])
->modifyBaseQueryUsing(static fn (Builder $query) => $query->withoutGlobalScope('published'));2
3
4
5
6
7
default() menerima bentuk array yang sama dengan request dan divalidasi dengan cara yang sama. query() menggantikan seluruh constraint dan menerima rule yang sudah disanitasi, berupa triple ['constraint' => Constraint, 'operator' => ConstraintOperator, 'value' => mixed], bukan raw request array. Karena itu custom query() pada filter jenis ini biasanya jarang diperlukan.
Constraint
Constraint mewakili satu kolom yang boleh digunakan pengguna dalam rule. Empat tipe tersedia secara bawaan.
| Class | inputType() | Untuk |
|---|---|---|
PandaPanel\Tables\Filters\Constraints\TextConstraint | text | string |
PandaPanel\Tables\Filters\Constraints\NumberConstraint | number | kolom numerik |
PandaPanel\Tables\Filters\Constraints\DateConstraint | date | tanggal dan datetime |
PandaPanel\Tables\Filters\Constraints\BooleanConstraint | none | flag boolean |
inputType() menjadi atribut type pada value input yang dirender frontend. none berarti operator sudah membawa jawabannya sendiri sehingga tidak ada input value yang ditampilkan.
Semua constraint memiliki API berikut:
use Illuminate\Database\Eloquent\Builder;
use PandaPanel\Tables\Enums\ConstraintOperator;
use PandaPanel\Tables\Filters\Constraints\Constraint;
Constraint::make(string $name): static
Constraint::label(string $label): static
Constraint::column(string $column): static // when the database column differs from the name
Constraint::getName(): string
Constraint::getLabel(): string // Str::headline($name) unless labelled
Constraint::getColumn(): string // $column ?? $name
Constraint::operators(): array // list<ConstraintOperator>
Constraint::inputType(): string
Constraint::supports(ConstraintOperator $operator): bool
Constraint::accepts(ConstraintOperator $operator, mixed $value): bool
Constraint::apply(Builder $query, ConstraintOperator $operator, mixed $value): void
Constraint::toArray(): array2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use PandaPanel\Tables\Filters\Constraints\TextConstraint;
// The request says "reference"; the query touches `orders.slug`.
TextConstraint::make('reference')->label('Order reference')->column('slug');2
3
4
Operator yang tersedia untuk setiap constraint
| Operator | Dibaca sebagai | TextConstraint | NumberConstraint | DateConstraint | BooleanConstraint |
|---|---|---|---|---|---|
Contains | contains | ✓ | |||
DoesNotContain | does not contain | ✓ | |||
StartsWith | starts with | ✓ | |||
EndsWith | ends with | ✓ | |||
EqualTo | is | ✓ | ✓ | ✓ | |
NotEqualTo | is not | ✓ | ✓ | ||
GreaterThan | is after | ✓ | ✓ | ||
GreaterThanOrEqual | is at least | ✓ | ✓ | ||
LessThan | is before | ✓ | ✓ | ||
LessThanOrEqual | is at most | ✓ | ✓ | ||
IsFilled | is filled | ✓ | ✓ | ✓ | ✓ |
IsBlank | is blank | ✓ | ✓ | ✓ | ✓ |
IsTrue | is true | ✓ | |||
IsFalse | is false | ✓ |
Kolom "Dibaca sebagai" di atas menunjukkan label manusia yang dikembalikan ConstraintOperator::label(). Nilai enum dan label runtime tetap seperti pada source API.
Bentuk query dari setiap operator
PandaPanel\Tables\Enums\ConstraintOperator adalah satu-satunya sumber operator dan setiap case dipetakan ke satu builder call.
| Case | Value | Builder call | needsValue() |
|---|---|---|---|
Contains | contains | where($c, 'like', '%value%') | true |
DoesNotContain | does_not_contain | whereNot($c, 'like', '%value%') | true |
StartsWith | starts_with | where($c, 'like', 'value%') | true |
EndsWith | ends_with | where($c, 'like', '%value') | true |
EqualTo | equal_to | where($c, '=', $value) | true |
NotEqualTo | not_equal_to | where($c, '!=', $value) | true |
GreaterThan | greater_than | where($c, '>', $value) | true |
GreaterThanOrEqual | greater_than_or_equal | where($c, '>=', $value) | true |
LessThan | less_than | where($c, '<', $value) | true |
LessThanOrEqual | less_than_or_equal | where($c, '<=', $value) | true |
IsFilled | is_filled | whereNotNull($c) | false |
IsBlank | is_blank | whereNull($c) | false |
IsTrue | is_true | where($c, '=', true) | false |
IsFalse | is_false | where($c, '=', false) | false |
Tiga operator LIKE meng-escape \, %, dan _ pada value, sehingga term yang mengandung wildcard dicocokkan secara literal dan tidak mengubah pola query.
ConstraintOperator::label() mengembalikan representasi manusia pada tabel sebelumnya. needsValue() menentukan apakah frontend perlu menampilkan value input dan apakah accepts() wajib menerima sebuah value.
Value yang diterima constraint
// Constraint (base): a value is required unless the operator carries its own answer.
public function accepts(ConstraintOperator $operator, mixed $value): bool
{
return ! $operator->needsValue() || (is_scalar($value) && $value !== '');
}2
3
4
5
NumberConstraint mempersempit validasi menjadi is_numeric($value). Perbandingan dengan nilai non-numerik bukan query yang lebih sempit, melainkan query yang tidak bermakna, sehingga value ditolak alih-alih di-coerce menjadi nol. DateConstraint mensyaratkan string yang dapat diparsing strtotime(), karena value lain akan dibandingkan sebagai string dan tidak memiliki semantics tanggal yang benar.
Membuat constraint sendiri
Hanya dua abstract method yang wajib diimplementasikan:
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Tables\Constraints;
use PandaPanel\Tables\Enums\ConstraintOperator;
use PandaPanel\Tables\Filters\Constraints\Constraint;
final class StatusConstraint extends Constraint
{
public function inputType(): string
{
return 'text';
}
/**
* @return list<ConstraintOperator>
*/
public function operators(): array
{
return [
ConstraintOperator::EqualTo,
ConstraintOperator::NotEqualTo,
ConstraintOperator::IsBlank,
];
}
public function accepts(ConstraintOperator $operator, mixed $value): bool
{
return ! $operator->needsValue()
|| in_array($value, ['open', 'closed', 'archived'], true);
}
}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
Override apply() hanya ketika sebuah rule memerlukan comparison yang tidak dapat diekspresikan enum. Operator sudah diperiksa terhadap operators() sebelum method dipanggil, sehingga match di dalam implementasi Anda dapat dianggap total.
Data yang disimpan di URL
Rule berada di dalam filter map tabel, sehingga back, forward, refresh, dan bookmark tetap mempertahankannya seperti state tabel lainnya:
?filters[conditions][0][column]=name
&filters[conditions][0][operator]=contains
&filters[conditions][0][value]=ada
&filters[conditions][1][column]=created_at
&filters[conditions][1][operator]=greater_than
&filters[conditions][1][value]=2026-01-012
3
4
5
6
Mengaktifkan persistFiltersInSession() mengingat map tersebut bersama filter lainnya. Lihat State yang dipertahankan.
Indicator
Filter::indicator() membangun teks chip di server karena hanya filter yang mengetahui arti value-nya. Untuk query builder, indicator terdiri dari label, titik dua, lalu seluruh rule yang lolos digabung dengan and:
Advanced: Name contains ada and Registered is after 2026-01-01Rule yang dibuang tidak muncul di indicator. Ini menjadi sinyal visual bahwa condition tersebut tidak diterapkan.
Definisi yang diserialisasi
toArray() menambahkan dua key ke definisi base filter:
[
'name' => 'conditions',
'label' => 'Advanced',
'type' => 'query_builder',
'default' => null,
'constraints' => [
[
'name' => 'name',
'label' => 'Name',
'input' => 'text',
'operators' => [
['value' => 'contains', 'label' => 'contains', 'needsValue' => true],
// ...
],
],
],
'maxRules' => 10,
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Tidak ada closure, query, atau model class yang dikirim. Frontend hanya dapat merender kolom dan comparison yang diberikan dan tidak dapat membuat rule baru di luar declaration.
Catatan
- Nested group AND/OR sengaja tidak tersedia. Fitur tersebut memerlukan recursive schema di server dan frontend beserta UI yang sesuai. Flat list dengan AND sudah menjawab sebagian besar kebutuhan tabel. Gunakan
FormFilterjika bentuk condition sudah diketahui, atau customquery()jika benar-benar membutuhkan strategi lain. - Constraint menunjuk kolom pada tabel yang sedang di-query. Tidak ada traversal relation.
TextConstraint::make('author.name')akan masuk ke builder sebagai table-qualified column, bukanwhereHas. Cari relation dengan dotted searchable column atau gunakanFormFilterdenganwhereHas. DateConstraintmenerima apa pun yang dapat diparsingstrtotime(), termasuk relative string sepertiyesterday. Ini disengaja dan value tetap menjadi bound parameter. Jika constraint harus menerima calendar date saja, overrideaccepts().IsTruedanIsFalsemembandingkan terhadaptruedanfalse. FlagNULLtidak cocok dengan keduanya; gunakanIsBlankuntuk mencari nilai null.- Rule yang dibuang tidak menghasilkan error. Pengguna melihat condition hilang dari indicator. Trade-off ini disengaja: alternatifnya adalah memperbaiki rule menjadi query yang tidak pernah diminta pengguna.
maxRules()melakukan truncation. Rule setelah batas dibuang pada saat parsing, bukan membuat seluruh filter gagal.