Validation
Validation pada form menggunakan validation rules Laravel yang diturunkan dari declaration field yang sama dengan yang digunakan untuk merender control. Gunakan halaman ini ketika Anda perlu menambahkan rules, mengetahui rules bawaan sebuah field, atau memahami check apa yang dilakukan browser sebelum request dikirim. Browser-side validation tidak menggantikan server: check di browser hanya membantu UX, sedangkan seluruh data tetap divalidasi ulang di server.
Contoh minimal
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rule;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\FormSchema;
$schema = FormSchema::make()->schema([
TextInput::make('email')
->email()
->required()
->rulesUsing(static fn (?Model $record): array => [
$record === null
? Rule::unique('users', 'email')
: Rule::unique('users', 'email')->ignore($record->getKey()),
]),
]);
$schema->validationRules();
// ['email' => ['required', 'string', 'email', 'max:255', Illuminate\Validation\Rules\Unique]]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Resource Page melakukan proses ini untuk Anda. Pada dasarnya validation-nya adalah:
validator($input, $schema->validationRules($record))->validate();Cara rule list sebuah field dibangun
Field::validationRules(?Model $record): list<mixed> menggabungkan empat sumber dengan urutan:
'required'atau'nullable', berdasarkanrequired().- Rules bawaan field type melalui
typeRules(). - Rules yang diberikan lewat
rules(). - Rules yang dihasilkan
rulesUsing().
use PandaPanel\Forms\Components\TextInput;
TextInput::make('slug')
->required() // required(bool $required = true): static
->rules(['alpha_dash']); // rules(list<mixed> $rules): static
// ['required', 'string', 'max:255', 'alpha_dash']2
3
4
5
6
required(false) bukan no-op. Method ini memasukkan nullable di awal rule list agar optional field yang kosong tidak gagal karena rule setelahnya.
rulesUsing()
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rule;
use PandaPanel\Forms\Components\TextInput;
TextInput::make('email')->rulesUsing(
static fn (?Model $record): array => [
Rule::unique('users', 'email')->ignore($record?->getKey()),
],
);2
3
4
5
6
7
8
9
Signature:
rulesUsing(Closure(?Model): list<mixed> $callback): staticRecord bernilai null pada create page. Hook ini digunakan untuk rules yang harus mengetahui record yang sedang diedit, misalnya unique rule yang harus mengabaikan record itu sendiri.
Rules bawaan setiap field type
Setiap rule list di bawah selalu diawali required atau nullable.
| Field | Rules |
|---|---|
TextInput | string; email saat email(); max:{n} dari maxLength(), default 255; min:{n} dari minLength() |
Textarea | string; max:{n} dari maxLength(), default-nya tidak diset |
PasswordInput | string; confirmed saat confirmed() |
NumberInput | numeric, atau integer saat integer(); min:{n}; max:{n} |
HiddenInput | tidak memiliki type rule sendiri |
Checkbox, Toggle | boolean |
Select single dengan static options | Rule::in() terhadap option keys |
Select single dengan existsIn() atau relation | Rule::exists($table, $column) |
Select multiple | array, ditambah elementRules() pada field.* |
Radio | Rule::in() terhadap option keys |
CheckboxList | array, ditambah Rule::in() pada field.* |
ToggleButtons | Rule::in(), atau array + Rule::in() pada field.* saat multiple() |
DatePicker, DateTimePicker | date; after_or_equal:{minDate}; before_or_equal:{maxDate} |
TimePicker | date_format:H:i, atau date_format:H:i:s saat seconds() |
ColorPicker | string, regex: untuk hex color |
Slider | numeric, min:{min}, max:{max} dari range() |
TagsInput | array; max:{n} dari maxTags(); string dan max:{n} pada field.* dari maxLength(), default 50 |
KeyValue | array; max:{n} dari maxPairs() |
RichEditor, MarkdownEditor | string; max:{n} dari maxLength() |
CodeEditor | string; json ketika language adalah CodeLanguage::Json; max:{n} |
FileUpload | string, atau array + max:{maxFiles} dan string pada field.* saat multiple() |
Repeater | array; min:{n}; max:{n}; item schema pada field.*.name |
Builder | array; min:{n}; max:{n}; setiap entry divalidasi sesuai block |
CustomField | tidak memiliki rule bawaan; deklarasikan lewat rules() |
Rules untuk list dan nested fields
Laravel tidak menurunkan element rules dari rule parent secara otomatis. Karena itu field dengan value berupa list melaporkan rules element secara terpisah.
use PandaPanel\Forms\Components\CheckboxList;
use PandaPanel\Forms\Components\Repeater;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\FormSchema;
FormSchema::make()
->schema([CheckboxList::make('roles')->options(['a' => 'A', 'b' => 'B'])])
->validationRules();
// ['roles' => ['nullable', 'array'], 'roles.*' => [Rule::in(['a', 'b'])]]
FormSchema::make()
->schema([
Repeater::make('items')->schema([TextInput::make('title')->required()]),
])
->validationRules();
// ['items' => [...], 'items.*.title' => ['required', 'string', 'max:255']]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Dua overridable methods menghasilkan key nested tersebut:
| Method | Signature | Menghasilkan |
|---|---|---|
elementRules() | elementRules(): list<mixed> | field.* |
nestedRules() | nestedRules(?Model $record = null): array<string, list<mixed>> | Full path keyed rules, misalnya items.*.title |
FormSchema::validationRules() memanggil keduanya untuk setiap field. Custom field type yang mengimplementasikan method tersebut mendapatkan behavior yang sama seperti built-in field.
Confirmed password
password_confirmation bukan field yang harus Anda deklarasikan sendiri. PasswordInput::confirmed(bool $confirmed = true) membuat renderer menampilkan confirmation input sekaligus membuat schema menambahkan rule yang sesuai, sehingga UI dan validation tidak dapat berbeda.
use PandaPanel\Forms\Components\PasswordInput;
use PandaPanel\Forms\FormSchema;
FormSchema::make()
->schema([PasswordInput::make('password')->confirmed()->required()])
->validationRules();
// [
// 'password' => ['required', 'string', 'confirmed'],
// 'password_confirmation' => ['nullable', 'string'],
// ]2
3
4
5
6
7
8
9
10
Confirmation key harus ada pada rules agar rule confirmed memiliki value yang dapat dibandingkan.
Validation pada Builder entries
Rules Builder tidak dapat direpresentasikan sebagai flat rule set karena rules untuk entry ketiga bergantung pada type yang dideklarasikan entry tersebut. Builder::validateEntries(mixed $value, ?Model $record = null): array<string, list<string>> memvalidasi setiap entry berdasarkan block yang disebutkannya lalu mengembalikan error menggunakan path yang sama dengan yang dirender frontend.
use PandaPanel\Forms\Components\Builder;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\Support\Block;
$field = Builder::make('content')->blocks([
Block::make('paragraph')->schema([TextInput::make('body')->required()]),
]);
$field->validateEntries([
['type' => 'paragraph', 'data' => ['body' => '']],
['type' => 'nope', 'data' => []],
]);
// [
// 'content.0.data.body' => ['The body field is required.'],
// 'content.1.type' => ['This block is not one this field offers.'],
// ]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Entry yang menyebut block yang tidak pernah dideklarasikan Builder ditolak pada validation dan juga dibuang oleh mutate(). Dengan demikian entry tersebut tidak dapat lolos validation maupun masuk storage.
Wizard steps
Wizard divalidasi secara utuh saat final submit. Endpoint validation per-step hanya tersedia agar user tidak dipindahkan ke step berikutnya sebelum step saat ini valid.
$schema->validationRulesForStep(0); // hanya rules milik step pertama
$schema->validationRules(); // seluruh step dalam satu rule set2
validationRulesForStep(int $step, ?Model $record = null): array mengambil subset dari complete form rules. Step sudah mengetahui field yang dimilikinya, sehingga deklarasi rules kedua hanya akan membuka peluang inkonsistensi. Confirmation key ikut step tempat password berada. Form tanpa Wizard mengembalikan [].
Endpoint:
POST {resource}/create/step
POST {resource}/{record}/edit/step2
Route name:
…validateCreateStep
…validateEditStep2
Response:
200 {"errors": []}
422 {"errors": {…}}2
Authorization menggunakan canCreate() atau canEdit($record). Step di luar range menghasilkan 422. Form tanpa Wizard menghasilkan 400. URL dikirim ke Page sebagai validateStepUrl dan bernilai null pada flat form.
Check yang dilakukan browser
Setiap field mengirim validation hints, yaitu subset rules yang dapat diperiksa browser secara jujur. Hints tersebut tetap diturunkan dari declaration yang sama dengan server rules.
| Hint | Dihasilkan oleh |
|---|---|
required | required() |
email | rule email |
numeric | rule numeric atau integer |
url | rule url |
min | rule min:{n} dengan numeric argument |
max | rule max:{n} dengan numeric argument |
confirmed | rule confirmed |
TextInput::make('email')->required()->email()->maxLength(100)
->toArray(null, 'create')['validation'];
// ['required' => true, 'email' => true, 'max' => 100.0]2
3
Rules yang membutuhkan database sengaja tidak dikirim sebagai frontend hints. Browser yang mencoba menebak hasil unique akan mudah salah. Hanya string rules yang dibaca; Rule object tidak menghasilkan hint. Field yang hidden oleh condition juga dilewati di browser dan server.
Posisi validation pada Page lifecycle
$input = $this->beforeValidate($request->all());
$data = $this->afterValidate(
validator($input, $schema->validationRules($record))->validate(),
);2
3
4
beforeValidate() menerima raw request, sedangkan afterValidate() menerima data yang sudah lolos validation. Keduanya berasal dari PandaPanel\Resources\Concerns\HasLifecycleHooks. Seluruh proses downstream membaca $data, bukan request langsung, sehingga key tanpa rule tidak dapat mencapai write.
Catatan
requiredpada field hanyalah marker UX di frontend. Menghapus attribute tersebut di browser tidak mengubah server rule.- Hidden field tidak divalidasi.
FormSchema::fields()mengecualikannya, sehingga tidak ada rule yang dibangun dan crafted request tidak dapat membuat field tersebut menjadi valid. Ini berlaku untukhiddenOn(),visibleOn(), serta Closurevisible()/hidden(). - Relation-backed Select divalidasi dengan
exists, bukanin. Rendered options hanya satu page dari table yang mungkin memiliki ribuan row. Validating terhadap current page akan menolak key valid yang kebetulan belum tampil. - Static option list adalah whitelist.
Select,Radio,CheckboxList, danToggleButtonsmenambahkanRule::in()berdasarkan declared keys. Empty option list tidak menambahkan rule tersebut. ColorPickermemvalidasi submitted value sebagai hex.swatches()dan hydration dapat mengenalirgb()/hsl(), tetapi form submission tetap harus cocok dengan hex regex kecuali Anda mengubah rule.- Dua field dengan nama sama menghasilkan exception.
validationRules()melakukan uniqueness assertion dan melemparPandaPanel\Exceptions\PanelSchemaException, karena hanya satu rule dapat bertahan pada key yang sama. TextInputmemiliki defaultmax:255walaupun Anda tidak menuliskannya.maxLength()default ke 255. GunakanmaxLength(null)untuk menghapus batas.- Validation bukan authorization. User yang tidak memiliki
canCreate()ataucanEdit()ditolak sebelum rules dibangun.