Setiap cookie yang terpasang di browser pengguna bermula dari satu baris header Set-Cookie pada respons yang Anda kirim. Artinya, mengonfigurasi cookie adalah pekerjaan di level respons — persis seperti mengatur status code atau Content-Type yang sudah kita bahas dua chapter sebelumnya. Teori lengkap tiap opsi cookie (Secure, HttpOnly, SameSite, dan teman-temannya) sudah kita bedah pada chapter cookie options di section HTTP state. Fokus chapter ini adalah sisi mesinnya di PHP: cara kerja bentuk array setcookie(), perhitungan waktu kedaluwarsa, pola penghapusan cookie yang benar, pengaruh path dan domain terhadap cakupan cookie, pengiriman banyak cookie sekaligus, dan konfigurasi cookie yang dipasang otomatis oleh session_start().

Opsi Lengkap pada setcookie() #

Sejak PHP 7.3, setcookie() menerima argumen kedua berupa array asosiatif yang jauh lebih mudah dibaca daripada rantai parameter posisi. Setiap kunci array menjadi satu opsi cookie:

php
<?php
$berhasil = setcookie("tema", "gelap", [
    "expires" => 0,
    "path" => "/",
    "httponly" => true,
    "samesite" => "Lax",
]);

var_dump($berhasil);
var_dump(headers_list());

Output yang Dihasilkan:

text
bool(true)
array(0) {
}

Mari kita bedah contoh di atas:

  1. Baris setcookie(...): fungsi mengembalikan bool(true) karena pemanggilan dilakukan sebelum output apa pun — syarat yang sama dengan header() pada chapter mengirim HTTP headers .
  2. "expires" => 0: nilai n berarti cookie hidup selama browser terbuka (session cookie); begitu browser ditutup, cookie lenyap sendiri.
  3. var_dump(headers_list()): mengembalikan array kosong di CLI karena SAPI CLI membuang header yang dipasang — sama seperti perilaku headers_list() pada chapter tersebut. Di SAPI web, header Set-Cookie benar-benar terkirim; kita akan membuktikannya dengan server sungguhan di bagian akhir chapter.

Nilai "expires" yang tidak nol menuntut perhitungan waktu. Bagaimana cara menghitungnya dengan benar? Mari kita bahas khusus.

Menghitung Waktu Kedaluwarsa #

Pola yang paling sering Anda temui di kode nyata adalah time() + 3600 untuk “satu jam dari sekarang”. Pola tersebut bekerja karena expires menerima Unix timestamp — jumlah detik sejak 1 Januari 1970. Contoh berikut memakai waktu beku agar hasilnya bisa ditampilkan secara deterministik:

php
<?php
$sekarang = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
$jamKeDepan = $sekarang->modify("+1 hour");

printf("sekarang  : %s (epoch %d)\n", $sekarang->format("Y-m-d H:i:s T"), $sekarang->getTimestamp());
printf("+1 jam    : %s (epoch %d)\n", $jamKeDepan->format("Y-m-d H:i:s T"), $jamKeDepan->getTimestamp());
printf("selisih   : %d detik\n", $jamKeDepan->getTimestamp() - $sekarang->getTimestamp());

Output yang Dihasilkan:

text
sekarang  : 2026-01-15 08:00:00 WIB (epoch 1768438800)
+1 jam    : 2026-01-15 09:00:00 WIB (epoch 1768442400)
selisih   : 3600 detik

Tiga baris output menunjukkan hubungan yang perlu Anda pahami: getTimestamp() selalu menghasilkan epoch UTC (1768438800) walau objek dibuat dengan zona Asia/Jakarta, karena epoch memang tidak mengenal zona waktu. Selisih 3600 detik itulah yang menjadi umur cookie, sedangkan tanggal berformat 2026-01-15 08:00:00 WIB hanyalah tampilan objek. Saat menuliskan tanggal ke dalam header, format yang sah adalah format GMT seperti Thu, 15 Jan 2026 02:00:00 GMT — kita lakukan konversi dengan setTimezone(new DateTimeZone("UTC")) pada contoh berikut.

setcookie() pada akhirnya membangun satu baris header dari seluruh opsi yang Anda berikan. Fungsi murni berikut meniru proses tersebut dan mengembalikan barisnya, sehingga seluruh keputusan konfigurasi bisa diuji tanpa server:

php
<?php
function headerSetCookie(string $nama, string $nilai, array $opsi = []): string
{
    $sekarang = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
    $kedaluwarsa = $sekarang->modify("+1 hour")->setTimezone(new DateTimeZone("UTC"));

    $header = rawurlencode($nama) . "=" . rawurlencode($nilai);
    $header .= "; expires=" . $kedaluwarsa->format("D, d M Y H:i:s") . " GMT";
    $header .= "; Max-Age=3600";
    $header .= "; path=" . ($opsi["path"] ?? "/");
    if (!empty($opsi["domain"])) {
        $header .= "; domain=" . $opsi["domain"];
    }
    if (!empty($opsi["secure"])) {
        $header .= "; Secure";
    }
    if (!empty($opsi["httponly"])) {
        $header .= "; HttpOnly";
    }
    $header .= "; SameSite=" . ($opsi["samesite"] ?? "Lax");
    return $header;
}

echo headerSetCookie("tema", "gelap", ["httponly" => true]) . "\n";
echo headerSetCookie("sesi", "abc123", [
    "path" => "/admin",
    "domain" => "toko.example.com",
    "secure" => true,
    "samesite" => "Strict",
]) . "\n";

Output yang Dihasilkan:

text
tema=gelap; expires=Thu, 15 Jan 2026 02:00:00 GMT; Max-Age=3600; path=/; HttpOnly; SameSite=Lax
sesi=abc123; expires=Thu, 15 Jan 2026 02:00:00 GMT; Max-Age=3600; path=/admin; domain=toko.example.com; Secure; SameSite=Strict

Mari kita bedah fungsinya baris per baris:

  1. Baris setTimezone(new DateTimeZone("UTC")): menggeser zona waktu objek ke UTC sebelum diformat, sehingga 08:00 WIB tercetak sebagai 02:00 GMT — format tanggal header HTTP memang selalu GMT.
  2. rawurlencode($nama) . "=" . rawurlencode($nilai): nama dan nilai cookie di-escape agar karakter seperti spasi atau ; tidak merusak struktur header.
  3. expires dan Max-Age ditulis berdampingan: expires berupa tanggal mutlak (dipahami klien lawas), Max-Age=3600 berupa umur relatif dalam detik (dipakai klien modern). Saat keduanya ada, Max-Age yang menang — dan nilai relatif ini kebal salah zona waktu.
  4. Rantai if untuk domain, Secure, dan HttpOnly: atribut hanya ditulis bila opsinya diisi, meniru perilaku setcookie() yang menghilangkan atribut kosong.

Cookie tidak bisa “dihapus” dari server secara langsung — server hanya bisa meminta browser menimpanya. Pola yang berlaku: kirim ulang cookie bernama sama dengan nilai kosong dan waktu kedaluwarsa di masa lalu:

php
<?php
function headerHapusCookie(string $nama, string $path = "/", string $domain = ""): string
{
    $header = rawurlencode($nama) . "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0";
    $header .= "; path=$path";
    if ($domain !== "") {
        $header .= "; domain=$domain";
    }
    return $header;
}

echo headerHapusCookie("tema") . "\n";
echo headerHapusCookie("sesi", "/admin", "toko.example.com") . "\n";

Output yang Dihasilkan:

text
tema=; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; path=/
sesi=; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; path=/admin; domain=toko.example.com

Tanggal Thu, 01 Jan 1970 00:00:00 GMT adalah epoch nol — titik nol waktu Unix — dan Max-Age=0 memerintahkan kedaluwarsa seketika. Browser yang menerima header ini akan membuang cookie bernama sama, bukan menyimpan yang baru.

Cakupan path dan domain #

Opsi path dan domain menentukan cookie tidak dikirim ke semua permintaan — browser menyaringnya. Aturan pencocokan path mengikuti RFC 6265: cookie terkirim bila path permintaan sama dengan path cookie, atau path cookie adalah awalan yang diikuti karakter /:

php
<?php
function cookieTerkirimUntukPath(string $pathCookie, string $pathPermintaan): bool
{
    if ($pathCookie === "/") {
        return true;
    }
    if (!str_starts_with($pathPermintaan, $pathCookie)) {
        return false;
    }
    $sisa = substr($pathPermintaan, strlen($pathCookie));
    return $sisa === "" || $sisa[0] === "/";
}

function domainCookieCocok(string $hostPermintaan, string $domainCookie): bool
{
    $hostPermintaan = strtolower($hostPermintaan);
    $domainCookie = strtolower($domainCookie);

    return $hostPermintaan === $domainCookie
        || str_ends_with($hostPermintaan, "." . $domainCookie);
}

$ujiPath = [
    ["/keranjang", "/keranjang"],
    ["/keranjang", "/keranjang/tambah"],
    ["/keranjang", "/keranjang-belanja"],
    ["/", "/admin/laporan"],
];
foreach ($ujiPath as [$pathCookie, $pathPermintaan]) {
    $status = cookieTerkirimUntukPath($pathCookie, $pathPermintaan) ? "terkirim" : "tidak";
    printf("path cookie %-12s permintaan %-18s => %s\n", $pathCookie, $pathPermintaan, $status);
}

$ujiDomain = [
    ["api.toko.example.com", "toko.example.com"],
    ["toko.example.com", "toko.example.com"],
    ["tokolain.example.org", "toko.example.com"],
];
foreach ($ujiDomain as [$host, $domain]) {
    $status = domainCookieCocok($host, $domain) ? "cocok" : "tidak";
    printf("host %-22s domain cookie %-18s => %s\n", $host, $domain, $status);
}

Output yang Dihasilkan:

text
path cookie /keranjang   permintaan /keranjang         => terkirim
path cookie /keranjang   permintaan /keranjang/tambah  => terkirim
path cookie /keranjang   permintaan /keranjang-belanja => tidak
path cookie /            permintaan /admin/laporan     => terkirim
host api.toko.example.com   domain cookie toko.example.com   => cocok
host toko.example.com       domain cookie toko.example.com   => cocok
host tokolain.example.org   domain cookie toko.example.com   => tidak

Dua baris pertama output membuktikan aturan awalan: /keranjang/tambah cocok karena sisa path (/tambah) diawali garis miring. Baris ketiga adalah jebakan klasik — /keranjang-belanja tidak cocok meski tampak seperti awalan, karena sisa -belanja diawali tanda hubung, bukan /. Sementara itu, pencocokan domain bekerja dua arah: cookie domain toko.example.com terkirim dari host itu sendiri maupun seluruh subdomainnya seperti api.toko.example.com — berguna untuk berbagi sesi login antar subdomain, sekaligus alasan cookie domain tidak boleh dipasang terlalu luas.

Tidak ada header Set-Cookie gabungan — setiap cookie adalah baris headernya sendiri, dan respons boleh membawa baris Set-Cookie sebanyak yang diperlukan. Pola kumpulan berikut membangun seluruh baris dalam satu fungsi:

php
<?php
function kumpulanSetCookie(array $preferensi, string $path = "/"): array
{
    $headers = [];
    foreach ($preferensi as $nama => $nilai) {
        $headers[] = "Set-Cookie: " . rawurlencode($nama) . "=" . rawurlencode($nilai)
            . "; path=$path; HttpOnly; SameSite=Lax";
    }
    return $headers;
}

foreach (kumpulanSetCookie(["tema" => "gelap", "bahasa" => "id", "tampilan" => "kompak"]) as $header) {
    echo $header . "\n";
}

Output yang Dihasilkan:

text
Set-Cookie: tema=gelap; path=/; HttpOnly; SameSite=Lax
Set-Cookie: bahasa=id; path=/; HttpOnly; SameSite=Lax
Set-Cookie: tampilan=kompak; path=/; HttpOnly; SameSite=Lax

Saat pengiriman dilakukan lewat header() alih-alih setcookie(), ingat pelajaran chapter mengirim HTTP headers : parameter kedua harus false agar panggilan kedua menambah baris baru, bukan mengganti baris Set-Cookie sebelumnya. Fungsi setcookie() tidak menghadapi masalah ini karena setiap panggilannya otomatis menjadi baris tersendiri.

session_start() memasang cookie sesi (PHPSESSID) secara otomatis, dan seluruh perilakunya dikendalikan direktif php.ini yang bisa dibaca lewat ini_get():

php
<?php
foreach ([
    "session.cookie_lifetime" => "umur cookie sesi dalam detik",
    "session.cookie_path" => "path tempat cookie sesi dikirim",
    "session.cookie_domain" => "domain cookie sesi",
    "session.cookie_secure" => "kirim hanya lewat HTTPS",
    "session.cookie_httponly" => "larang akses dari JavaScript",
    "session.cookie_samesite" => "pelindung pengiriman lintas situs",
] as $kunci => $makna) {
    printf("%-26s = %-4s ; %s\n", $kunci, var_export(ini_get($kunci), true), $makna);
}

Output yang Dihasilkan:

text
session.cookie_lifetime    = '0'  ; umur cookie sesi dalam detik
session.cookie_path        = '/'  ; path tempat cookie sesi dikirim
session.cookie_domain      = ''   ; domain cookie sesi
session.cookie_secure      = '0'  ; kirim hanya lewat HTTPS
session.cookie_httponly    = '0'  ; larang akses dari JavaScript
session.cookie_samesite    = ''   ; pelindung pengiriman lintas situs

Nilai bawaan tersebut jelas tidak ideal untuk produksi: session.cookie_httponly masih '0', session.cookie_secure masih '0', dan session.cookie_samesite kosong. Aplikasi nyata sebaiknya menyetel ketiganya lebih ketat — baik lewat php.ini/.htaccess, maupun per-skrip sebelum session_start() memakai session_set_cookie_params():

php
<?php
session_set_cookie_params([
    "lifetime" => 0,
    "path" => "/",
    "domain" => "toko.example.com",
    "secure" => true,
    "httponly" => true,
    "samesite" => "Lax",
]);

$params = session_get_cookie_params();
foreach (["lifetime", "path", "domain", "secure", "httponly", "samesite"] as $kunci) {
    printf("%-9s => %s\n", $kunci, var_export($params[$kunci], true));
}

Output yang Dihasilkan:

text
lifetime  => 0
path      => '/'
domain    => 'toko.example.com'
secure    => true
httponly  => true
samesite  => 'Lax'

Bentuk array session_set_cookie_params() (tersedia sejak PHP 7.3, sama seperti setcookie()) menyetel parameter cookie untuk session yang akan dimulai — panggilan harus dilakukan sebelum session_start(). Setelah itu, session_get_cookie_params() mengonfirmasi keenam nilai benar-benar berubah: secure dan httponly kini true, samesite terisi 'Lax'. Daftar lengkap direktif session beserta efeknya dibahas pada chapter session configuration .

Menguji di Server Web Sungguhan #

Perilaku lengkap setcookie() baru terlihat di SAPI web. Server bawaan PHP cukup untuk membuktikannya:

php
<?php
// pasang-cookie.php — dari folder berkas ini, jalankan:
//   php -S 127.0.0.1:8092
// lalu amati header responsnya:
//   curl -i http://127.0.0.1:8092/pasang-cookie.php
setcookie("tema", "gelap", ["expires" => 0, "path" => "/", "httponly" => true, "samesite" => "Lax"]);
setcookie("bahasa", "id", ["expires" => 0, "path" => "/", "samesite" => "Lax"]);

echo "dua cookie terpasang\n";

Saat kita jalankan curl -i terhadap skrip tersebut, respons berstatus HTTP/1.1 200 OK membawa tepat dua baris Set-Cookie: tema=gelap; path=/; HttpOnly; SameSite=Lax dan bahasa=id; path=/; SameSite=Lax — atributnya persis mengikuti opsi yang kita berikan, atribut kosong tidak ditulis. Perhatikan pula bahwa tidak ada expires maupun Max-Age pada keduanya karena expires => 0 memang tidak menghasilkan atribut kedaluwarsa. Matikan server setelah selesai dengan Ctrl+C atau pkill -f "php -S".

Ringkasan #

setcookie() bentuk-array menerjemahkan opsi menjadi satu baris header Set-Cookie: expires dihitung sebagai Unix timestamp (dengan Max-Age sebagai padanan relatif yang lebih tahan salah zona waktu), path dan domain mempersempit cookie sesuai aturan pencocokan awalan path dan subdomain, dan penghapusan cookie menuntut nilai kosong plus kedaluwarsa masa lalu dengan path/domain yang persis sama. Cookie sesi yang dipasang session_start() dikendalikan direktif session.cookie_* yang sebaiknya diperketat lewat session_set_cookie_params(). Setelah cookie, pertanyaan berikutnya adalah seberapa lama respons itu sendiri boleh disimpan oleh klien — jawabannya ada di header Cache-Control, topik chapter berikutnya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contoh.php

Unduh berkas
php
<?php
// contoh.php — ringkasan konfigurasi cookie dari sisi respons.
// Semua fungsi murni mengembalikan string header sehingga seluruh
// keputusan konfigurasi bisa diperiksa tanpa server web.

function headerSetCookie(string $nama, string $nilai, array $opsi = []): string
{
    $sekarang = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
    $kedaluwarsa = $sekarang->modify("+1 hour")->setTimezone(new DateTimeZone("UTC"));

    $header = rawurlencode($nama) . "=" . rawurlencode($nilai);
    $header .= "; expires=" . $kedaluwarsa->format("D, d M Y H:i:s") . " GMT";
    $header .= "; Max-Age=3600";
    $header .= "; path=" . ($opsi["path"] ?? "/");
    if (!empty($opsi["domain"])) {
        $header .= "; domain=" . $opsi["domain"];
    }
    if (!empty($opsi["secure"])) {
        $header .= "; Secure";
    }
    if (!empty($opsi["httponly"])) {
        $header .= "; HttpOnly";
    }
    $header .= "; SameSite=" . ($opsi["samesite"] ?? "Lax");
    return $header;
}

function headerHapusCookie(string $nama, string $path = "/", string $domain = ""): string
{
    $header = rawurlencode($nama) . "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0";
    $header .= "; path=$path";
    if ($domain !== "") {
        $header .= "; domain=$domain";
    }
    return $header;
}

echo "=== Cookie preferensi (HttpOnly, SameSite=Lax) ===\n";
echo headerSetCookie("tema", "gelap", ["httponly" => true]) . "\n";

echo "=== Cookie sesi admin (Secure, SameSite=Strict) ===\n";
echo headerSetCookie("sesi", "abc123", [
    "path" => "/admin",
    "domain" => "toko.example.com",
    "secure" => true,
    "samesite" => "Strict",
]) . "\n";

echo "=== Menghapus cookie dengan path/domain yang sama ===\n";
echo headerHapusCookie("sesi", "/admin", "toko.example.com") . "\n";

echo "=== Parameter cookie sesi yang akan dipakai session_start() ===\n";
session_set_cookie_params([
    "lifetime" => 0,
    "path" => "/",
    "domain" => "toko.example.com",
    "secure" => true,
    "httponly" => true,
    "samesite" => "Lax",
]);
$params = session_get_cookie_params();
foreach (["lifetime", "path", "domain", "secure", "httponly", "samesite"] as $kunci) {
    printf("%-9s => %s\n", $kunci, var_export($params[$kunci], true));
}