Dasar validasi

Metode validate(), aturan bawaan, dan pengalihan dengan pesan error.

Daftar isi

Validasi (validation) adalah pemeriksaan data masukan sebelum data digunakan, misalnya memastikan judul tidak kosong dan kategori yang dipilih benar-benar tersedia. Topik ini membahas metode validate() pada objek Request, kumpulan aturan bawaan yang sering digunakan, serta pengalihan dengan pesan error yang menyertai kegagalan validasi. Seluruh mekanisme diuji pada aplikasi catatan sehingga perilaku formulir saat data valid dan saat data tidak lolos dapat diamati dari keluaran nyata.

Metode validate() pada request#

Metode validate() menerima satu array yang memasangkan nama kolom dengan aturannya, lalu memeriksa seluruh data permintaan terhadap aturan itu. Apabila semua aturan lolos, metode ini mengembalikan array data yang telah diperiksa, sehingga aksi controller melanjutkan pekerjaannya dengan data yang dijamin memenuhi aturan tersebut. Metode ini tersedia pada objek Request yang dioperkan ke aksi controller, tanpa perlu membuat objek validator secara manual.

Yang juga penting dipahami adalah perilakunya ketika ada aturan yang dilanggar. Metode validate() melempar exception berisi daftar pelanggaran, dan exception handler Laravel mengubahnya menjadi respons pengalihan kembali ke formulir bila permintaannya berasal dari browser. Pesan error dan masukan pengguna ikut disimpan secara flash ke session pada saat yang sama, sehingga baris kode setelah validate() tidak pernah dijalankan untuk data yang tidak lolos.

php Validasi masukan pada aksi controller
// tidak-dijalankan
public function store(Request $request)
{
    $data = $request->validate([
        'judul' => ['required', 'string', 'max:20'],
        'isi' => ['required', 'string'],
        'category_id' => ['required', 'integer', 'in:1,2,3'],
    ]);

    Note::create($data);

    return redirect('/catatan')->with('sukses', 'Catatan berhasil disimpan.');
}

Pada contoh di atas, kolom judul wajib terisi, berupa teks, dan panjangnya paling banyak 20 karakter. Kolom category_id wajib berupa bilangan bulat yang nilainya 1, 2, atau 3. Apabila data memenuhi semuanya, array $data hanya memuat kolom yang divalidasi, sehingga aman dioperkan ke Note::create(). Apabila ada yang gagal, Laravel sudah mengalihkan pengguna kembali dengan pesan error sebelum baris Note::create() sempat dijalankan.

Aturan bawaan yang sering digunakan#

Laravel menyediakan puluhan aturan bawaan, dan hampir semua formulir cukup menggunakan sebagian kecilnya. Aturan ditulis sebagai string di dalam array, dan satu kolom boleh diperiksa oleh beberapa aturan sekaligus. Tabel berikut merangkum aturan yang paling sering muncul pada aplikasi nyata.

AturanSyarat yang diperiksa
requiredKolom wajib ada dan tidak boleh kosong
stringNilai berupa teks
integerNilai berupa bilangan bulat
max:20Paling banyak 20 karakter untuk teks, atau bernilai paling tinggi 20 untuk angka
min:3Paling sedikit 3 karakter untuk teks, atau bernilai paling rendah 3 untuk angka
emailNilai berformat alamat email
in:1,2,3Nilai termasuk salah satu daftar yang diperbolehkan
exists:categories,idNilai ada pada kolom id tabel categories
unique:categories,namaNilai belum digunakan pada kolom nama tabel categories
confirmedNilai sama dengan kolom <nama>_confirmation

Aturan exists dan unique mengirim query ke database pada setiap pemeriksaan. Perilaku tersebut dapat dicoba langsung melalui Validator::make(), yaitu mesin validasi yang juga digunakan oleh validate() secara internal, hanya tanpa pengalihan otomatis. Hasil pemeriksaannya dibaca melalui metode fails() dan errors().

bash
# mencoba aturan validasi melalui Validator
cd ~/aplikasi-catatan
php artisan tinker --execute='
use App\Models\Category;
use Illuminate\Support\Facades\Validator;
Category::firstOrCreate(["nama" => "Kerja"]);
$validator = Validator::make(
    ["nama" => "Kerja"],
    ["nama" => ["required", "max:50", "unique:categories,nama"]]
);
echo "gagal: " . var_export($validator->fails(), true) . PHP_EOL;
print_r($validator->errors()->all());
$validator = Validator::make(
    ["nama" => "Inspirasi"],
    ["nama" => ["required", "max:50", "unique:categories,nama"]]
);
echo "gagal: " . var_export($validator->fails(), true) . PHP_EOL;
'
text Keluaran
gagal: true
Array
(
    [0] => The nama has already been taken.
)
gagal: false

Pemeriksaan pertama menolak nama Kerja karena kategori dengan nama itu sudah ada pada tabel, dan pesan errornya menyebutkan persis alasannya. Pemeriksaan kedua lolos karena nama Inspirasi belum digunakan. Pola fails() dan errors()->all() inilah yang juga bekerja di dalam validate(), hanya tanpa pengalihan otomatis.

Pengalihan dengan pesan error#

Saat validasi gagal pada permintaan dari browser, exception handler Laravel mengembalikan respons pengalihan ke halaman sebelumnya. Daftar pesan error disimpan secara flash ke session, lalu middleware pembagi error milik grup web menyediakannya ke setiap view sebagai variabel $errors. Masukan pengguna diperlakukan dengan cara yang sama, sehingga old() yang dibahas pada topik data siklus permintaan otomatis terisi tanpa perlu memanggil withInput() secara manual.

View memanfaatkan variabel tersebut melalui direktif @error untuk satu kolom atau $errors->any() untuk seluruh formulir. Formulir contoh berikut menampilkan pesan error tepat di bawah kolom yang bermasalah, mempertahankan isian melalui old(), dan menandai pilihan kategori yang tersimpan.

bash
# menyiapkan view formulir contoh pada proyek aplikasi catatan
cd ~/aplikasi-catatan
mkdir -p resources/views/uji-validasi
cat > resources/views/uji-validasi/formulir.blade.php <<'BLADE'
<form method="POST" action="/uji/validasi">
    @csrf

    <label for="judul">Judul</label>
    <input id="judul" name="judul" type="text" value="{{ old('judul') }}">
    @error('judul')
        <p>{{ $message }}</p>
    @enderror

    <label for="isi">Isi</label>
    <textarea id="isi" name="isi" rows="3">{{ old('isi') }}</textarea>
    @error('isi')
        <p>{{ $message }}</p>
    @enderror

    <label for="category_id">Kategori</label>
    <select id="category_id" name="category_id">
        <option value="">(pilih)</option>
        @foreach ([1 => 'Kerja', 2 => 'Belajar', 3 => 'Pribadi'] as $id => $nama)
            <option value="{{ $id }}" @selected(old('category_id') == $id)>{{ $nama }}</option>
        @endforeach
    </select>
    @error('category_id')
        <p>{{ $message }}</p>
    @enderror

    <button type="submit">Simpan catatan</button>
</form>

@if (session('sukses'))
    <p>{{ session('sukses') }}</p>
@endif
BLADE

Perilaku lengkapnya diuji dengan skrip berikut, yang menjalankan aplikasi contoh tanpa server web dan mengirim tiga permintaan POST berurutan dengan isi yang berbeda. Setiap permintaan yang gagal diikuti dengan pembukaan ulang formulir menggunakan session yang sama, sehingga pesan error dan nilai old() terbaca dari HTML yang dirender.

php uji-validasi.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

$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();

Route::middleware([
    Illuminate\Session\Middleware\StartSession::class,
    Illuminate\View\Middleware\ShareErrorsFromSession::class,
])->group(function () {
    Route::view('/uji/validasi/formulir', 'uji-validasi.formulir');

    Route::post('/uji/validasi', function (Request $request) {
        $data = $request->validate([
            'judul' => ['required', 'string', 'max:20'],
            'isi' => ['required', 'string'],
            'category_id' => ['required', 'integer', 'in:1,2,3'],
        ]);

        return redirect('/uji/validasi/formulir')
            ->with('sukses', 'Data valid: '.$data['judul']);
    });
});

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

function teruskanCookie($respons, Request $request): Request
{
    foreach ($respons->headers->getCookies() as $cookie) {
        $request->cookies->set($cookie->getName(), $cookie->getValue());
    }

    return $request;
}

function bukaFormulir($kernel, $responsSebelumnya): string
{
    $request = Request::create('http://localhost/uji/validasi/formulir', 'GET');
    teruskanCookie($responsSebelumnya, $request);

    return $kernel->handle($request)->getContent();
}

function daftarError(string $html): array
{
    preg_match_all('/<p>([^<]+)<\/p>/', $html, $cocok);

    return $cocok[1];
}

// skenario 1: POST tanpa satu pun data
$resp1 = $kernel->handle(Request::create('http://localhost/uji/validasi', 'POST', [], [], [], [
    'HTTP_REFERER' => 'http://localhost/uji/validasi/formulir',
]));
echo '1. POST tanpa data: status '.$resp1->getStatusCode().', alihan ke '.$resp1->headers->get('Location').PHP_EOL;
$html1 = bukaFormulir($kernel, $resp1);
foreach (daftarError($html1) as $i => $pesan) {
    echo '   error '.($i + 1).': '.$pesan.PHP_EOL;
}
preg_match('/name="judul" type="text" value="([^"]*)"/', $html1, $judul1);
echo '   old judul: '.($judul1[1] === '' ? '(kosong)' : $judul1[1]).PHP_EOL;

// skenario 2: category_id di luar daftar yang diperbolehkan
$resp2 = $kernel->handle(Request::create('http://localhost/uji/validasi', 'POST', [
    'judul' => 'Rapat pekanan',
    'isi' => 'Mencatat keputusan rapat.',
    'category_id' => '9',
], [], [], ['HTTP_REFERER' => 'http://localhost/uji/validasi/formulir']));
echo '2. POST category_id 9: status '.$resp2->getStatusCode().PHP_EOL;
$html2 = bukaFormulir($kernel, $resp2);
foreach (daftarError($html2) as $i => $pesan) {
    echo '   error '.($i + 1).': '.$pesan.PHP_EOL;
}
preg_match('/name="judul" type="text" value="([^"]*)"/', $html2, $judul2);
echo '   old judul: '.($judul2[1] === '' ? '(kosong)' : $judul2[1]).PHP_EOL;

// skenario 3: data lengkap dan memenuhi semua aturan
$resp3 = $kernel->handle(Request::create('http://localhost/uji/validasi', 'POST', [
    'judul' => 'Rapat pekanan',
    'isi' => 'Mencatat keputusan rapat.',
    'category_id' => '2',
], [], [], ['HTTP_REFERER' => 'http://localhost/uji/validasi/formulir']));
echo '3. POST data lengkap: status '.$resp3->getStatusCode().PHP_EOL;
$html3 = bukaFormulir($kernel, $resp3);
echo '   pesan sukses: '.(str_contains($html3, 'Data valid: Rapat pekanan') ? 'Data valid: Rapat pekanan' : '(tidak ada)').PHP_EOL;
text Keluaran
1. POST tanpa data: status 302, alihan ke http://localhost/uji/validasi/formulir
   error 1: The judul field is required.
   error 2: The isi field is required.
   error 3: The category id field is required.
   old judul: (kosong)
2. POST category_id 9: status 302
   error 1: The selected category id is invalid.
   old judul: Rapat pekanan
3. POST data lengkap: status 302
   pesan sukses: Data valid: Rapat pekanan

Skenario pertama menunjukkan tiga pesan error sekaligus, dan urutannya mengikuti urutan aturan pada validate(). Skenario kedua menunjukkan aturan in menolak nilai 9, sementara isian judul dipertahankan melalui old() tanpa pemanggilan withInput() apa pun. Skenario ketiga lolos seluruh aturan sehingga aksi melanjutkan pekerjaannya dan menulis pesan sukses ke session.

Kesalahan umum#

Kesalahan pertama adalah mengharapkan pengalihan pada permintaan JSON. Saat permintaan mengharapkan respons JSON, misalnya dari aplikasi frontend terpisah, kegagalan validasi tidak dialihkan melainkan dijawab dengan status 422 beserta daftar pesan error dalam bentuk JSON. Aplikasi yang memanggil endpoint tersebut perlu membaca struktur itu, bukan mengikuti pengalihannya.

Kesalahan kedua adalah salah memahami aturan max dan min pada kolom angka. Aturan ini mengukur panjang nilai untuk teks, tetapi membandingkan nilainya untuk angka, sehingga integer dengan max:20 menolak angka 21, bukan angka dengan 21 digit. Pastikan aturannya sesuai dengan jenis nilai yang diperiksa sebelum digunakan.

Kesalahan ketiga adalah memasang required pada kolom yang sebenarnya boleh dikosongkan. Aturan required menolak permintaan yang tidak mengirim kolomnya sama sekali, sehingga formulir tanpa pilihan kategori ikut ditolak. Untuk kolom yang boleh kosong, gunakan nullable sebagai pengganti required, lalu biarkan aturan lain hanya bekerja ketika ada nilainya. Apabila kolomnya tetap perlu diperiksa ketika diisi, pasangkan nullable dengan aturan lain pada array yang sama, misalnya 'category_id' => ['nullable', 'integer', 'in:1,2,3'].

Ringkasan#

  • Metode validate() memeriksa data permintaan terhadap array aturan, mengembalikan data yang lolos, dan melempar exception yang diubah menjadi pengalihan dengan pesan error saat gagal.
  • Aturan bawaan seperti required, string, integer, max, min, in, exists, dan unique mencakup kebutuhan formulir pada umumnya, dan Validator::make() adalah mesinnya yang dapat dipanggil langsung.
  • Pesan error tersedia di view melalui $errors dan direktif @error, sedangkan masukan pengguna otomatis tersedia bagi old().
  • Kegagalan validasi pada permintaan JSON dijawab dengan status 422 berisi pesan error, bukan pengalihan.

Referensi#