Router sudah memilih handler yang tepat, tetapi handler belum tahu apa isi request yang dibawa klien: body JSON, query string, atau header. Parsing adalah proses mengubah byte mentah tersebut menjadi struktur data PHP yang aman digunakan. Pada Chapter ini kita akan membaca body dari php://input, mendeteksi Content-Type, mengurai query string dan header, lalu menegaskan batas tegas antara tahap parsing dan tahap validasi. Semua contoh memakai string JSON tetap sehingga bisa langsung diuji di CLI.
Anatomi Request API #
Sepanjang section ini, request kita wakili sebagai array murni dengan bentuk tetap — kelanjutan pola [status, headers, body] dari Chapter Routing dan Endpoints
. Berikut wujud array $request yang diterima setiap handler:
{
"method": "POST",
"path": "/tasks",
"query": {"page": "2"},
"headers": {"Content-Type": "application/json"},
"body": {"title": "Belajar PHP"}
}Ada dua alasan bentuk ini dipilih. Pertama, seluruh isi request menjadi data biasa yang mudah dibuat di test tanpa server sungguhan. Kedua, handler tidak pernah menyentuh superglobal langsung, sehingga perilakunya bisa diprediksi dan diganti sumber datanya kapan pun. Pada server nyata, array ini diisi dari $_GET, $_SERVER, dan php://input oleh kode penghubung — persis seperti contoh glue code pada chapter sebelumnya.
Membaca Body JSON #
Body request tidak tersedia di variabel PHP mana pun secara otomatis; body harus dibaca dari stream php://input. Pada server nyata pola bakunya tampak seperti ini:
<?php
// Pada server nyata, body mentah dibaca dari stream php://input:
$raw = file_get_contents("php://input");
// $raw berisi string seperti '{"title": "Belajar PHP"}'
$body = json_decode($raw, true);Untuk contoh yang bisa diuji di CLI, kita bungkus logikanya menjadi fungsi parseJsonBody() yang menerima string mentah. Fungsi ini mengembalikan null untuk semua masukan yang tidak berhasil menjadi array PHP — penanda bahwa body tidak layak diproses lebih jauh:
<?php
function parseJsonBody(string $raw): ?array
{
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
return null;
}
return $decoded;
}
$raw = '{"title": "Review pull request", "assignee": "[email protected]"}';
$body = parseJsonBody($raw);
echo $body["title"] . " — " . $body["assignee"] . "\n";
var_dump(parseJsonBody("bukan-json"));
var_dump(parseJsonBody('"hanya sebuah string"'));Output yang Dihasilkan:
Review pull request — [email protected]
NULL
NULLMari kita bedah perilakunya:
json_decode($raw, true): argumen keduatruememinta hasil berupa array asosiatif, bukan objectstdClass. Array lebih mudah diproses dengan fungsi array PHP.is_array($decoded): JSON yang valid belum tentu objek — bisa juga string, angka, atau list. Pemeriksaan ini menyaring semua bentuk non-array sekaligus menangkap JSON rusak yang menghasilkannull.- Nilai balik
?array: handler pemanggil cukup memeriksa=== nulluntuk tahu apakah body layak diproses.
Ada satu celah kecil yang perlu Anda sadari: list JSON seperti ["a", "b"] lolos pemeriksaan is_array() karena memang ter-decode menjadi array. Jika API Anda hanya menerima objek di body, tambahkan pemeriksaan array_is_list($decoded) dan tolak juga bentuk list tersebut.
Menangani JSON Rusak dengan json_last_error #
Ketika json_decode() gagal, PHP tidak melempar exception — fungsi hanya mengembalikan null dan diam-diam mencatat kode error internal. Fungsi json_last_error() membaca kode tersebut, sedangkan json_last_error_msg() memberikan pesan yang bisa dibaca manusia:
<?php
$inputs = [
'{"title": "Belajar PHP"}',
'{"title": "Belajar PHP",}',
"{'title': 'kutip tunggal'}",
];
foreach ($inputs as $index => $raw) {
json_decode($raw, true);
if (json_last_error() === JSON_ERROR_NONE) {
echo ($index + 1) . ". valid\n";
} else {
echo ($index + 1) . ". error " . json_last_error() . ": " . json_last_error_msg() . "\n";
}
}Output yang Dihasilkan:
1. valid
2. error 4: Syntax error
3. error 4: Syntax errorDua masukan terakhir sama-sama menghasilkan kode 4 (JSON_ERROR_SYNTAX): koma menggantung di akhir objek dan tanda kutip tunggal yang bukan pembatas string JSON. Kode error yang sering muncul di praktik cukup sedikit, sehingga tabel ringkas berikut cukup membantu:
| Konstanta | Nilai | Arti |
|---|---|---|
JSON_ERROR_NONE | 0 | Tidak ada error |
JSON_ERROR_DEPTH | 1 | Struktur bersarang melebihi kedalaman maksimum |
JSON_ERROR_SYNTAX | 4 | Sintaks JSON tidak valid |
JSON_ERROR_UTF8 | 5 | Karakter UTF-8 tidak bersih |
Satu kebiasaan penting: panggil json_last_error() segera setelah json_decode(). Kode error ini bersifat global dan akan tertimpa oleh operasi JSON berikutnya, jadi menunda pemeriksaan berarti membaca status yang basi.
Content-Type sebagai Petunjuk Parsing #
Sebelum mengurai body, handler perlu tahu formatnya. Petunjuknya ada di header Content-Type. API modern lazimnya hanya menerima JSON, tetapi klien lama — atau form HTML biasa — masih mengirim application/x-www-form-urlencoded. Fungsi parseBody() berikut menangani kedua format itu:
<?php
function parseBody(string $contentType, string $raw): ?array
{
if (str_contains($contentType, "application/json")) {
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : null;
}
if (str_contains($contentType, "application/x-www-form-urlencoded")) {
$form = [];
parse_str($raw, $form);
return $form;
}
return null;
}
$jsonBody = parseBody("application/json; charset=utf-8", '{"title": "Deploy ke produksi"}');
$formBody = parseBody(
"application/x-www-form-urlencoded",
"title=Review+PR&labels[]=urgent&labels[]=api"
);
echo $jsonBody["title"] . "\n";
echo $formBody["title"] . ": " . implode(", ", $formBody["labels"]) . "\n";Output yang Dihasilkan:
Deploy ke produksi
Review PR: urgent, apiDua detail layak digarisbawahi. Pertama, pencocokan memakai str_contains() alih-alih perbandingan === karena nilai Content-Type sering membawa parameter tambahan seperti ; charset=utf-8 — perbandingan persis akan salah menolak request yang sah. Kedua, parse_str() mengisi array hasil lewat parameter kedua; memanggilnya tanpa parameter kedua membuat variabel dinamis di scope saat ini dan membuka celah keamanan yang tidak perlu. Perhatikan juga format form menulis labels[] untuk menghasilkan array — kebalikan dari JSON yang menyatakan array secara eksplisit.
Query String untuk Filter dan Paginasi #
Parameter di URL setelah tanda tanya — misalnya ?status=done&page=2 — dibawa di query string. Fungsi parseQuery() membungkus parse_str() agar hasilnya selalu array yang aman:
<?php
function parseQuery(string $queryString): array
{
$params = [];
parse_str($queryString, $params);
return $params;
}
$request = [
"method" => "GET",
"path" => "/tasks",
"query" => parseQuery("status=done&tag[]=php&tag[]=api&page=2"),
];
echo "status: " . $request["query"]["status"] . "\n";
echo "tag: " . implode(", ", $request["query"]["tag"]) . "\n";
$page = max(1, (int) ($request["query"]["page"] ?? 1));
echo "halaman: " . $page . "\n";Output yang Dihasilkan:
status: done
tag: php, api
halaman: 2Semua nilai query string datang sebagai string, sama seperti hasil regex pada chapter routing. Baris max(1, (int) ...) menunjukkan idiom penting: konversi ke integer sekaligus menjepit nilai minimal, sehingga page=0, page=-3, atau page=abc semuanya jatuh aman ke halaman 1. Untuk parameter berulang seperti tag, gunakan akhiran [] agar parse_str() menghasilkan array seperti pada output. Query string adalah rumah alami bagi filter, sorting, dan paginasi — pola lengkapnya kita bahas di Chapter Pagination, Filtering, dan Sorting
.
Membaca Header Request #
Header membawa metadata request: Content-Type, Authorization, hingga penelusuran seperti X-Request-Id. Nama header secara teknis case-sensitive saat dikirim, tetapi praktiknya klien bisa memakai kapitalisasi apa pun, jadi handler sebaiknya membandingkan nama header secara case-insensitive:
<?php
function headerOf(array $request, string $name): ?string
{
$name = strtolower($name);
foreach ($request["headers"] as $key => $value) {
if (strtolower($key) === $name) {
return $value;
}
}
return null;
}
$request = [
"method" => "POST",
"path" => "/tasks",
"query" => [],
"headers" => [
"Content-Type" => "application/json",
"X-Request-Id" => "req-2026-0001",
],
"body" => ["title" => "Belajar PHP"],
];
echo "request id: " . (headerOf($request, "x-request-id") ?? "-") . "\n";
echo "content type: " . (headerOf($request, "CONTENT-TYPE") ?? "-") . "\n";Output yang Dihasilkan:
request id: req-2026-0001
content type: application/jsonFungsi headerOf() menormalkan kedua sisi perbandingan dengan strtolower() sebelum mencocokkan, lalu mengembalikan null bila header tidak ada — pemanggil menentukan nilai cadangan dengan ??. Pada server nyata, header request bisa diambil dari $_SERVER dengan kunci berprefix HTTP_ (misalnya HTTP_X_REQUEST_ID), atau lewat getallheaders() pada server yang mendukungnya; teori lengkapnya ada di Chapter Headers
.
Batas Parsing dan Batas Validasi #
Tahap parsing berhenti di “apakah struktur data ini bisa dibaca?”. Pertanyaan “apakah isinya masuk akal?” adalah urusan validasi. Memisahkan keduanya membuat pesan error jauh lebih jelas: 400 untuk body yang tidak bisa diurai, 422 untuk field yang tidak lolos aturan bisnis. Handler berikut menunjukkan kedua tahap itu berdampingan:
<?php
function respond(int $status, array $payload): array
{
return [
$status,
["Content-Type: application/json; charset=utf-8"],
json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
}
function createTaskHandler(array $request): array
{
$body = $request["body"];
if ($body === null) {
return respond(400, [
"error" => ["code" => "INVALID_BODY", "message" => "Body bukan JSON objek valid"],
]);
}
$title = $body["title"] ?? null;
if (!is_string($title) || trim($title) === "") {
return respond(422, [
"error" => ["code" => "VALIDATION_ERROR", "message" => "Field title wajib diisi"],
]);
}
return respond(201, [
"data" => ["id" => 42, "title" => trim($title), "done" => false],
]);
}
$requests = [
["body" => null, "label" => "body rusak"],
["body" => ["title" => " "], "label" => "title kosong"],
["body" => ["title" => "Belajar request parsing"], "label" => "valid"],
];
foreach ($requests as $request) {
[$status, $headers, $body] = createTaskHandler($request);
echo $request["label"] . " => " . $status . " " . $body . "\n";
}Output yang Dihasilkan:
body rusak => 400 {"error":{"code":"INVALID_BODY","message":"Body bukan JSON objek valid"}}
title kosong => 422 {"error":{"code":"VALIDATION_ERROR","message":"Field title wajib diisi"}}
valid => 201 {"data":{"id":42,"title":"Belajar request parsing","done":false}}Tiga request tiruan memperlihatkan tiga nasib berbeda: body rusak ditolak 400 sebelum validasi menyentuhnya, title berisi spasi lolos parsing tetapi gagal aturan isi (422), dan body sehat diterima 201. Di aplikasi nyata, blok pemeriksaan $title itulah tempatnya memanggil layer validasi yang lebih kaya — aturan panjang, format email, dan sebagainya, yang akan kita bangun di Chapter Request Validation
.
Membatasi Ukuran Body #
Request tanpa batas ukuran adalah pintu masuk serangan denial-of-service. Header Content-Length bisa diperiksa lebih dulu — sebelum body dibaca — untuk menolak request yang terlalu besar dengan status 413 Payload Too Large:
<?php
function bodyLimitStatus(?string $contentLength, int $maxBytes): ?int
{
if ($contentLength === null || !ctype_digit($contentLength)) {
return null;
}
return (int) $contentLength > $maxBytes ? 413 : null;
}
$maxBytes = 64 * 1024;
var_dump(bodyLimitStatus("2048", $maxBytes));
var_dump(bodyLimitStatus("131072", $maxBytes));
var_dump(bodyLimitStatus("abc", $maxBytes));
var_dump(bodyLimitStatus(null, $maxBytes));Output yang Dihasilkan:
NULL
int(413)
NULL
NULLFungsi bodyLimitStatus() mengembalikan null saat keputusan tidak bisa diambil — header hilang atau bukan angka — dan 413 hanya saat angkanya jelas melebihi jatah. Pemeriksaan ctype_digit() sekaligus menolak nilai seperti "12abc" yang berbahaya jika langsung dikonversi. Ingat bahwa Content-Length bersifat deklaratif: di belakang reverse proxy, batas yang sesungguhnya sering ditegakkan oleh proxy itu sendiri, dan di PHP konfigurasi post_max_size juga membatasi ukuran body yang diproses. Tetap menolak lebih awal di kode membuat API Anda jujur soal batasannya sendiri.
Ringkasan #
Parsing request berarti mengubah php://input, query string, dan header menjadi array PHP yang terkendali: json_decode() dengan pemeriksaan json_last_error(), parse_str() untuk query dan form, serta pencocokan header case-insensitive. Batas tegas antara parsing (400) dan validasi (422) menjaga pesan error tetap akurat. Setelah handler mengerti isi request, tugas berikutnya adalah membalasnya — mengubah data PHP menjadi respons JSON yang konsisten, yang menjadi fokus Chapter Response Serialization
.