Sampai titik ini middleware kita kebanyakan menjawab pertanyaan ya atau tidak: boleh lanjut, atau ditolak. Padahal posisinya di depan controller membuat middleware juga tempat yang tepat untuk mengolah request sebelum controller melihatnya — menambah data, menyetel ulang header, bahkan menolak dengan format khusus — dan tetap punya kesempatan terakhir menyentuh response setelah controller selesai. Chapter ini melatih keempat posisi tersebut dengan satu contoh nyata masing-masing, ditutup panduan memilih antara middleware dan controller.

Menambah Data ke Request Sebelum Controller #

Method merge() menambah pasangan kunci-nilai ke input request, seolah-olah dikirim oleh pengguna:

php
// app/Http/Middleware/AddRequestContext.php
public function handle(Request $request, Closure $next): Response
{
    $request->merge(['sumber' => 'middleware']);

    $request->mergeIfMissing(['bahasa' => 'id']);

    $request->headers->set('X-Request-Id', 'req-123');

    $response = $next($request);

    $response->headers->set('X-Processed-By', 'middleware');

    return $response;
}

Tiga baris pertama bekerja di sisi before: merge() selalu menimpa, mergeIfMissing() hanya mengisi bila kunci belum ada, dan headers->set() mengganti header request. Dua baris setelah $next bekerja di sisi after dengan menandai response. Route berikut membaca kembali ketiga nilai itu:

php
Route::get('/konteks', function (Request $request) {
    return sprintf(
        'sumber=%s bahasa=%s request_id=%s',
        $request->input('sumber'),
        $request->input('bahasa'),
        $request->header('X-Request-Id'),
    );
})->middleware(AddRequestContext::class);

Feature test berikut membandingkan request tanpa query string dan request dengan ?bahasa=en:

php
public function test_middleware_menambah_data_request(): void
{
    $response = $this->get('/konteks');

    $response->assertStatus(200);

    echo 'konteks_default: isi="', $response->getContent(),
        '", header X-Processed-By="', $response->headers->get('X-Processed-By'), '"', PHP_EOL;
}

Output yang Dihasilkan:

text
konteks_default: isi="sumber=middleware bahasa=id request_id=req-123", header X-Processed-By="middleware"
konteks_bahasa_en: isi="sumber=middleware bahasa=en request_id=req-123"

Perhatikan baris kedua: query ?bahasa=en dari pengguna tetap menang karena mergeIfMissing tidak menimpa nilai yang sudah ada — pengaturan middleware hanya menjadi nilai cadangan.

Sifat penting dari merge(): data yang diinjeksi middleware menyatu dengan input pengguna, sehingga controller tidak bisa lagi membedakan mana yang datang dari query string dan mana buatan middleware. Itu aman untuk nilai internal seperti penanda sumber request, tetapi jangan dipakai untuk menyerobot input yang memang milik pengguna. Untuk data yang perlu dibedakan, simpan di attribute request melalui $request->attributes->set() yang bisa dibaca controller secara eksplisit.

Short-Circuit: Menolak Tanpa Menyentuh Controller #

Short-circuit pada chapter sebelumnya menolak begitu saja; sekarang kita beri bentuk jawaban yang informatif. Middleware EnsureClientVersion memeriksa header versi aplikasi mobile dan meminta pengguna memperbarui aplikasi:

php
// app/Http/Middleware/EnsureClientVersion.php
public function handle(Request $request, Closure $next): Response
{
    $versi = (int) $request->header('X-Client-Version', '1');

    if ($versi < 2) {
        return response()->json([
            'message' => 'Versi client minimal 2. Silakan perbarui aplikasi.',
        ], 426);
    }

    return $next($request);
}

Status 426 (Upgrade Required) menyampaikan maksud yang tepat: client boleh kembali, tetapi harus memakai versi baru. Test berikut mengirim request tanpa header lalu dengan header versi 3:

php
$lama = $this->get('/versi');
$baru = $this->get('/versi', ['X-Client-Version' => '3']);

echo 'versi_lama: status=', $lama->getStatusCode(), ', isi=', $lama->getContent(), PHP_EOL;
echo 'versi_baru: status=', $baru->getStatusCode(), ', isi="', $baru->getContent(), '"', PHP_EOL;

Output yang Dihasilkan:

text
versi_lama: status=426, isi={"message":"Versi client minimal 2. Silakan perbarui aplikasi."}
versi_baru: status=200, isi="Data versi 2"

Tanpa header, $versi jatuh ke default '1' dan controller tidak pernah dieksekusi — itulah inti short-circuit.

Terminating Middleware: Bekerja Setelah Response Terkirim #

Laravel mendukung method kedua bernama terminate($request, $response) yang dijalankan setelah response selesai disiapkan — cocok untuk pencatatan lambat seperti log akses atau metrik. Middleware tidak perlu registrasi khusus: cukup memiliki method terminate, dan Laravel memanggilnya otomatis:

php
// app/Http/Middleware/TerminateBenchmark.php
public function handle(Request $request, Closure $next): Response
{
    TraceLog::add('handle: sebelum controller');

    $response = $next($request);

    TraceLog::add('handle: sesudah controller');

    return $response;
}

public function terminate(Request $request, Response $response): void
{
    TraceLog::add('terminate: setelah response siap');
}

Output yang Dihasilkan (feature test mengakses /benchmark):

text
trace: handle: sebelum controller > controller: memproses > handle: sesudah controller > terminate: setelah response siap

Urutan jejak tersebut menegaskan posisi terminate: seluruh pipeline sampai response siap terlebih dahulu, baru method tersebut berjalan. Bedanya dengan logika after biasa terletak pada biaya dan momen. Kode setelah $next($request) masih menahan response sebelum dikirim ke pengguna, jadi pekerjaan berat di posisi itu menambah waktu tunggu. terminate berjalan setelah response siap dikirim, sehingga tugas seperti menulis log, mengukur durasi, atau membersihkan berkas sementara tidak membuat pengguna menunggu. Fitur ini pernah dijuluki terminable middleware di dokumentasi, dan tetap memakai objek middleware yang sama — tidak ada instance atau registrasi baru.

Middleware atau Controller? #

KebutuhanTempat tepat
Berlaku untuk banyak routeMiddleware
Hanya dipakai satu actionController
Mengubah data request lintas routeMiddleware
Validasi milik satu formForm Request / controller
Tugas berat setelah response terkirimterminate di middleware

Aturan praktisnya: kalau logika ini akan dipanggil lagi dari route lain, letakkan di middleware; kalau hanya milik satu halaman, controller tetap lebih jujur.

Empat posisi pengolahan — sebelum, sesudah, tolak cepat, terminate — kini lengkap di tangan kita. Chapter penutup section ini merangkai semuanya menjadi tiga filter praktis: whitelist IP, pemaksaan JSON, dan pencatatan request. Rujukan resminya ada di dokumentasi middleware .