Di chapter Route Parameters kita melihat bukti bahwa {id} menerima nilai apa pun: request GET /tugas/abc tetap cocok dan handler menerima abc sebagai ID. Dalam aplikasi nyata, handler tersebut kemungkinan besar akan mencari record bernomor abc di database dan gagal dengan cara yang tidak elegan. Route constraints menutup celah ini di lapisan paling awal: kita memasang aturan pada parameter, dan request yang parameternya melanggar aturan langsung ditolak router dengan 404 sebelum handler sempat berjalan. Chapter ini membahas constraint manual berbasis regular expression, helper bawaan seperti whereNumber() dan whereUuid(), constraint global, serta interaksinya dengan urutan pencocokan route.

Constraint dengan Regular Expression #

Method ->where() menerima nama parameter dan pola regular expression yang harus dipenuhi seluruh nilai segmen. Pola [0-9]+ berarti “satu atau lebih digit”:

php
<?php

use Illuminate\Support\Facades\Route;

// Constraint regex manual: id harus angka
Route::get('/tugas/{id}', function (string $id) {
    return "Tugas nomor {$id}";
})->where('id', '[0-9]+');

Saat request datang, router memeriksa segmen URI terhadap pola sebagai bagian dari pencocokan route. Segmen 12 lolos; segmen abc atau 12a gagal, dan router melanjutkan pencocokan ke route berikutnya — bila tidak ada yang cocok, hasilnya 404 seperti biasa. Constraint, dengan kata lain, mempersempit definisi “cocok”, bukan menghasilkan error berbeda.

Urutan Pencocokan dan Route Statis #

Constraint menjadi penyelamat pada satu jebakan klasik: route dinamis yang dideklarasikan sebelum route statis. Tambahkan route statis berikut tepat di bawah route /tugas/{id} dari bagian sebelumnya:

php
// Route statis setelah route dinamis: aman
Route::get('/tugas/arsip', fn () => 'Ini halaman arsip tugas.');

Router Laravel mencocokkan route berdasarkan urutan pendaftaran. Tanpa constraint, route /tugas/{id} yang terdaftar lebih dulu akan menelan URI /tugas/arsip — uji nyata kita menunjukkan handler mengembalikan Tugas nomor arsip, dan route statis di belakangnya tidak pernah terjangkau. Dengan ->where('id', '[0-9]+'), segmen arsip gagal pola sehingga pencocokan jatuh ke route statis. Urutan file di atas kini aman.

Constraint Bawaan #

Pola seperti “hanya angka” atau “hanya huruf” terlalu sering dipakai untuk ditulis manual, jadi Laravel menyediakan shortcut-nya:

MethodSetara denganContoh lolosContoh ditolak
whereNumber('x')[0-9]+42abc
whereAlpha('x')[a-zA-Z]+budibudi-01
whereAlphaNumeric('x')[a-zA-Z0-9]+TVLGD42TV-LGD42
whereUuid('x')pola UUID 8-4-4-4-12 digit hex3f8a2b1c-9d4e-...bukan-uuid
whereUlid('x')pola ULID 26 karakter01ARZ3NDEKTSV4RRFFQ69G5FAVbukan-ulid
whereIn('x', [...])alternasi nilai, mis. aktif|selesaiaktifpending

Polanya persis seperti yang didefinisikan framework — misalnya whereNumber() sungguh hanya membungkus pola [0-9]+. Satu tambahan yang berguna: whereIn() membatasi parameter ke daftar nilai tetap; uji pada route /status/{kode} dengan nilai ['aktif', 'selesai', 'tertunda'] menerima GET /status/aktif dengan 200 dan menolak GET /status/pending dengan 404. Berikut definisi lengkap constraint bawaan di routes/web.php:

php
// Shortcut bawaan untuk pola yang sering dipakai
Route::get('/notulen/{nomor}', fn (string $nomor) => "Notulen rapat ke-{$nomor}")
    ->whereNumber('nomor');

Route::get('/pengguna/{nama}', fn (string $nama) => "Nama: {$nama}")
    ->whereAlpha('nama');

Route::get('/produk/{kode}', fn (string $kode) => "Kode produk: {$kode}")
    ->whereAlphaNumeric('kode');

Route::get('/pesanan/{uuid}', fn (string $uuid) => "UUID: {$uuid}")
    ->whereUuid('uuid');

Route::get('/dokumen/{ulid}', fn (string $ulid) => "ULID: {$ulid}")
    ->whereUlid('ulid');

// Beberapa constraint sekaligus dalam satu array
Route::get('/pengguna/{nama}/tugas/{id}', function (string $nama, string $id) {
    return "Tugas {$id} milik {$nama}";
})->where(['nama' => '[a-z]+', 'id' => '[0-9]+']);

Blok terakhir menunjukkan bentuk ketiga: ->where() juga menerima array asosiatif saat beberapa parameter perlu dibatasi sekaligus. Kita uji seluruh constraint tersebut dengan skrip uji-route.php yang sama dengan chapter-chapter sebelumnya — daftar $permintaan diisi sebelas URI berikut:

Output yang Dihasilkan:

text
GET /tugas/12                                    → HTTP 200
GET /tugas/abc                                   → HTTP 404
GET /tugas/arsip                                 → HTTP 200
GET /notulen/5                                   → HTTP 200
GET /notulen/rapat                               → HTTP 404
GET /pengguna/budi                               → HTTP 200
GET /pengguna/budi-01                            → HTTP 404
GET /produk/TVLGD42                              → HTTP 200
GET /produk/TV-LGD42                             → HTTP 404
GET /pesanan/3f8a2b1c-9d4e-4f6a-8b7c-1a2b3c4d5e6f → HTTP 200
GET /dokumen/01ARZ3NDEKTSV4RRFFQ69G5FAV          → HTTP 200

Mari kita baca pasangan yang menarik. whereAlpha menolak budi-01 karena tanda hubung dan angka bukan huruf; whereAlphaNumeric menolak TV-LGD42 hanya karena tanda hubung, padahal huruf dan angkanya sah. UUID dan ULID lolos dengan nilai yang formatnya persis mengikuti standarnya. Perhatikan juga baris GET /tugas/arsip yang 200 — bukti route statis dari bagian sebelumnya memang terjangkau. Delapan route lengkapnya tampil di route:list:

bash
php artisan route:list --except-vendor

Output yang Dihasilkan:

text
  GET|HEAD       dokumen/{ulid} ................................................ routes/web.php:26
  GET|HEAD       notulen/{nomor} ............................................... routes/web.php:14
  GET|HEAD       pengguna/{nama} ............................................... routes/web.php:17
  GET|HEAD       pengguna/{nama}/tugas/{id} .................................... routes/web.php:30
  GET|HEAD       pesanan/{uuid} ................................................ routes/web.php:23
  GET|HEAD       produk/{kode} ................................................. routes/web.php:20
  GET|HEAD       tugas/arsip ................................................... routes/web.php:11
  GET|HEAD       tugas/{id} ..................................................... routes/web.php:6

                                                                                Showing [8] routes

URI di kolom kedua tetap menampilkan placeholder mentahnya — constraint adalah aturan pencocokan internal dan tidak mengubah bentuk URI.

Constraint Global dengan Route::pattern #

Ketika satu nama parameter selalu bermakna sama di seluruh aplikasi — misalnya {id} selalu berarti angka — Laravel memungkinkan polanya dideklarasikan sekali untuk semua route. Tempatnya adalah method boot() di app/Providers/AppServiceProvider.php:

php
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Route::pattern('id', '[0-9]+');
    }
}

Deklarasi Route::pattern('id', '[0-9]+') membuat setiap parameter bernama id di seluruh aplikasi otomatis terikat pola angka — tanpa ->where() pada route-nya:

php
// Tidak ada ->where() di sini: constraint datang dari pola global
Route::get('/tugas/{id}', function (string $id) {
    return "Tugas nomor {$id}";
});

Output yang Dihasilkan:

text
GET /tugas/42    → HTTP 200
GET /tugas/abc   → HTTP 404

Route di atas sama sekali tidak memiliki ->where(), namun abc tetap ditolak — pola global bekerja. Seluruh detail constraint tersedia di dokumentasi routing bagian Route Parameters.

Menguji Constraint dengan Feature Test #

Karena constraint berakhir pada status code, feature test adalah alat verifikasi yang paling lugas. Berikut tests/Feature/RouteConstraintTest.php beserta hasil eksekusinya di aplikasi uji:

php
<?php

namespace Tests\Feature;

use Tests\TestCase;

class RouteConstraintTest extends TestCase
{
    public function test_id_angka_diterima(): void
    {
        $response = $this->get('/tugas/12');

        $response->assertStatus(200);
        $response->assertSee('Tugas nomor 12');
    }

    public function test_id_bukan_angka_menyebabkan_404(): void
    {
        $response = $this->get('/tugas/abc');

        $response->assertStatus(404);
    }

    public function test_uri_statis_tidak_ditelan_route_dinamis(): void
    {
        $response = $this->get('/tugas/arsip');

        $response->assertStatus(200);
        $response->assertSee('halaman arsip');
    }
}
bash
php artisan test tests/Feature/RouteConstraintTest.php

Output yang Dihasilkan:

text
  PASS  Tests\Feature\RouteConstraintTest
  ✓  id angka diterima
  ✓  id bukan angka menyebabkan 404
  ✓  uri statis tidak ditelan route dinamis

  Tests:    3 passed (5 assertions)

Tiga test tersebut mengunci perilaku paling penting constraint: nilai sah diterima, nilai liar ditolak dengan 404, dan route statis tidak tertelan route dinamis. Kontrak semacam ini membuat refactoring urutan route di masa depan jauh lebih aman.

Dengan constraint, handler kita kini menerima id yang pasti angka. Langkah berikutnya sangat alami: daripada menerima angka lalu mencari recordnya sendiri, mengapa tidak meminta Laravel langsung mengirimkan record Eloquent sebagai parameter? Itulah route model binding , topik chapter berikutnya.