Aturan validasi kustom

Objek Rule dan pesan validasi yang disesuaikan kebutuhan aplikasi.

Daftar isi

Aturan bawaan Laravel tidak selalu mampu menyatakan syarat yang spesifik terhadap satu aplikasi, misalnya memastikan judul catatan belum digunakan pada kategori yang sama. Topik ini membahas dua mekanisme yang menyempurnakan validasi, yaitu pesan error yang ditulis sendiri beserta placeholder-nya dan objek Rule sebagai aturan yang dibuat sendiri oleh aplikasi. Keduanya diuji pada aplikasi catatan sehingga pesan yang dihasilkan dan keputusan pemeriksaan dapat diamati dari keluaran nyata.

Pesan validasi khusus per kolom#

Metode validate() dan Validator::make() menerima array pesan sebagai argumen ketiga. Kunci arraynya berformat nama kolom diikuti nama aturan, misalnya judul.required, sehingga satu pesan hanya berlaku untuk satu kolom dan satu aturan. Apabila kuncinya hanya nama aturan saja, misalnya required, pesan yang sama berlaku untuk semua kolom yang menggunakan aturan itu. Aturan yang tidak memiliki pesan khusus tetap menggunakan pesan bawaannya.

Pesan khusus dapat memuat placeholder yang diganti oleh Laravel pada saat pesan dirender. Placeholder :attribute menunjuk nama kolom yang sedang diperiksa, :max menunjuk batas nilai pada aturan max, dan :min bekerja dengan cara serupa. Nama pada :attribute berasal dari nama kolom yang diubah menjadi lebih mudah dibaca, misalnya category_id menjadi category id. Apabila aplikasi membutuhkan istilah lain, misalnya kategori, argumen keempat menyediakan peta pengganti nama per kolom.

bash
# mencoba pesan khusus dan nama atribut khusus melalui Validator
cd ~/aplikasi-catatan
php artisan tinker --execute='
use Illuminate\Support\Facades\Validator;
$validator = Validator::make(
    ["judul" => "", "category_id" => 99],
    ["judul" => ["required"], "category_id" => ["required", "integer", "in:1,2,3"]],
    ["judul.required" => "Kolom :attribute wajib diisi.", "category_id.in" => ":attribute yang dipilih tidak tersedia."],
    ["judul" => "judul catatan", "category_id" => "kategori"]
);
foreach ($validator->errors()->all() as $pesan) {
    echo $pesan.PHP_EOL;
}
'
text Keluaran
Kolom judul catatan wajib diisi.
kategori yang dipilih tidak tersedia.

Pesan pertama menunjukkan placeholder :attribute terisi judul catatan sesuai peta pada argumen keempat, dan pesan kedua mengganti category_id menjadi kategori. Tanpa pesan khusus, kedua pelanggaran itu ditampilkan dengan kalimat bawaan berbahasa Inggris. Pola ini cukup untuk penyesuaian sederhana, tetapi menjadi terbatas ketika syarat yang diperiksa tidak tersedia pada aturan bawaan, misalnya pemeriksaan yang perlu membaca database. Kebutuhan semacam itu ditangani oleh objek Rule pada bagian berikutnya.

Objek Rule dan perintah make:rule#

Aturan kustom ditulis dalam bentuk kelas yang mengimplementasikan interface ValidationRule. Perintah make:rule membuat kerangkanya pada direktori app/Rules beserta namespace App\Rules yang sesuai, sehingga aturan dapat dipanggil dari mana saja melalui nama kelasnya. Nama aturan dianjurkan menyatakan syarat yang diperiksa, misalnya JudulUnik untuk syarat judul belum digunakan.

bash
# tidak-dijalankan
# membuat kerangka aturan kustom
cd ~/aplikasi-catatan
php artisan make:rule JudulUnik
text Hasil perintah make:rule

   INFO  Rule [app/Rules/JudulUnik.php] created successfully.  

Kerangka yang dihasilkan memuat satu metode, yaitu validate(string $attribute, mixed $value, Closure $fail). Metode ini dijalankan untuk setiap nilai yang diperiksa. Pemanggilan $fail('pesan') menandai pelanggaran beserta pesannya, sedangkan metode yang berakhir tanpa memanggil $fail menandakan nilai lolos. Aturan juga boleh menerima argumen melalui konstruktor, misalnya id kategori yang menjadi batas pemeriksaan.

bash
# menuliskan aturan JudulUnik pada direktori app/Rules
cd ~/aplikasi-catatan
mkdir -p app/Rules
cat > app/Rules/JudulUnik.php <<'PHP'
<?php

namespace App\Rules;

use App\Models\Note;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Translation\PotentiallyTranslatedString;

class JudulUnik implements ValidationRule
{
    public function __construct(private readonly int $categoryId)
    {
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $dipakai = Note::where('judul', $value)
            ->where('category_id', $this->categoryId)
            ->exists();

        if ($dipakai) {
            $fail(':attribute sudah dipakai catatan lain pada kategori ini.');
        }
    }
}
PHP

Aturan JudulUnik memeriksa apakah judul yang dikirim sudah ada pada catatan lain dengan kategori yang sama. Konstruktor menyimpan id kategori, metode validate() mengirim query berdasarkan nilai itu, lalu exists() menentukan hasilnya. Pesan pada $fail memuat placeholder :attribute, sehingga nama kolom ikut tergantikan ketika pesan ditampilkan.

Menggunakan objek Rule pada pemeriksaan#

Objek aturan dipasang pada array aturan dengan kata kunci new, persis seperti aturan berupa string. Konstruktor diisi langsung pada tempat yang sama, sehingga satu pemanggilan dapat menargetkan satu kategori tertentu. Posisi objek di dalam array tidak mengubah keputusannya, tetapi letaknya menentukan urutan pesan apabila beberapa aturan dilanggar sekaligus.

Perilaku aturan diuji dengan skrip berikut yang menyiapkan kategori dan satu catatan secara deterministik, lalu menjalankan tiga pemeriksaan Validator::make(), yaitu judul yang sudah digunakan pada kategori yang sama, judul yang sama pada kategori berbeda, dan judul baru.

php uji-aturan-kustom.php
<?php

use App\Models\Category;
use App\Models\Note;
use App\Rules\JudulUnik;
use Illuminate\Support\Facades\Validator;

$root = getcwd();
if (! is_file($root.'/bootstrap/app.php')) {
    $root = getenv('HOME').'/aplikasi-catatan';
}

require $root.'/vendor/autoload.php';

$app = require $root.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

$kerja = Category::firstOrCreate(['nama' => 'Kerja']);
$belajar = Category::firstOrCreate(['nama' => 'Belajar']);
Note::firstOrCreate(
    ['judul' => 'Laporan mingguan', 'category_id' => $kerja->id],
    ['isi' => 'Rekap pekerjaan pekan ini.']
);

// skenario 1: objek Rule menolak judul yang sudah dipakai pada kategori yang sama
$validator = Validator::make(
    ['judul' => 'Laporan mingguan', 'category_id' => $kerja->id],
    ['judul' => ['required', 'string', 'max:20', new JudulUnik($kerja->id)]]
);
echo '1. duplikat pada kategori yang sama: gagal '.var_export($validator->fails(), true).PHP_EOL;
foreach ($validator->errors()->all() as $pesan) {
    echo '   '.$pesan.PHP_EOL;
}

// skenario 2: judul sama pada kategori berbeda
$validator = Validator::make(
    ['judul' => 'Laporan mingguan', 'category_id' => $belajar->id],
    ['judul' => ['required', 'string', 'max:20', new JudulUnik($belajar->id)]]
);
echo '2. judul sama pada kategori berbeda: gagal '.var_export($validator->fails(), true).PHP_EOL;

// skenario 3: judul baru pada kategori yang sama
$validator = Validator::make(
    ['judul' => 'Rencana harian', 'category_id' => $kerja->id],
    ['judul' => ['required', 'string', 'max:20', new JudulUnik($kerja->id)]]
);
echo '3. judul baru pada kategori yang sama: gagal '.var_export($validator->fails(), true).PHP_EOL;
text Keluaran
1. duplikat pada kategori yang sama: gagal true
   judul sudah dipakai catatan lain pada kategori ini.
2. judul sama pada kategori berbeda: gagal false
3. judul baru pada kategori yang sama: gagal false

Skenario pertama menunjukkan aturan menolak judul Laporan mingguan karena catatan dengan judul itu sudah ada pada kategori Kerja, dan pesannya memuat placeholder yang sudah tergantikan. Skenario kedua lolos karena kategori Belajar belum memiliki catatan dengan judul yang sama, meskipun judulnya identik. Skenario ketiga lolos karena judul Rencana harian memang belum digunakan. Semua keputusan itu hanya mungkin karena aturan membawa konteks kategori dari konstruktornya.

Kesalahan umum#

Kesalahan pertama adalah menulis kunci pesan dengan format yang keliru, misalnya required.judul padahal formatnya judul.required. Pesan dengan kunci yang tidak dikenali tidak pernah digunakan dan tidak memunculkan error apa pun, sehingga ciri khasnya adalah pesan bawaan berbahasa Inggris yang tetap muncul meskipun array pesan sudah diisi. Urutan yang benar adalah nama kolom lebih dahulu, diikuti nama aturannya. Periksa kuncinya bila pesan khusus tidak muncul.

Kesalahan kedua adalah membiarkan validate() berakhir tanpa memanggil $fail. Metode yang tidak memanggil $fail selalu dianggap lolos, sehingga aturan yang logikanya keliru tidak pernah menolak data dan kesalahannya tidak tampak. Setiap cabang bersyarat yang menolak nilai perlu memuat pemanggilan $fail. Ujilah aturan dengan data yang seharusnya ditolak agar keputusannya dapat dipastikan.

Kesalahan ketiga adalah menganggap :attribute menampilkan nama kolom mentah. Placeholder ini menampilkan nama kolom yang sudah diubah menjadi lebih mudah dibaca, bukan nama kolom persis seperti di database. Apabila aplikasi membutuhkan istilah lain, misalnya kategori untuk category_id, isi peta pada argumen keempat atau pada berkas terjemahan. Pesan pada $fail mendukung placeholder yang sama, sehingga pesan aturan kustom dan pesan bawaan tampil konsisten.

Ringkasan#

  • Array pesan pada argumen ketiga validate() atau Validator::make() mengganti pesan bawaan per kolom dan per aturan dengan kunci berformat kolom.aturan.
  • Placeholder seperti :attribute dan :max terisi otomatis, dan peta pada argumen keempat mengganti nama kolom dengan istilah aplikasi.
  • Aturan kustom berbentuk kelas yang mengimplementasikan interface ValidationRule dengan metode validate(), dan pemanggilan $fail menandai pelanggaran beserta pesannya.
  • Objek aturan dipasang pada array aturan dengan new, dapat menerima argumen melalui konstruktor, dan bekerja di semua tempat array aturan ditulis.

Referensi#