BAB 7: STRING MANIPULATION

7-3: Regular Expression

Objectives

  • Dasar-dasar regex
  • preg_match dan preg_replace
  • Pattern matching
  • Mengenal capture groups
  • Menghindari regex yang berbahaya (ReDoS)

Apa Itu Regular Expression?

Regular expression (regex) adalah pola (pattern) untuk mencari teks dengan aturan tertentu. Alih-alih menulis banyak strpos dan substr untuk memeriksa format, satu regex bisa menangani semuanya sekaligus.

Contoh sederhana: memvalidasi apakah sebuah string adalah email. Dengan function string biasa:

php
<?php

$email = "user@example.com";

// Cara manual: harus ada @, ada titik setelah @, dst.
if (
    str_contains($email, "@") &&
    str_contains($email, ".") &&
    strpos($email, "@") > 0 &&
    strpos($email, ".") > strpos($email, "@")
) {
    echo "Kira-kira email valid";
}

// Dengan regex: satu pola, sekali cek
if (preg_match("/^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$/", $email)) {
    echo "Email valid";
}

?>
┌─────────────────────────────────────────────────────┐
│             PATTERN REGEX                           │
│                                                     │
│   / ^ [a-z]+ @ [a-z]+ \. [a-z]{2,} $ /             │
│     │   │      │  │      │  │      │               │
│   awal  huruf  @  huruf  .  huruf  akhir           │
│                                                     │
│   "andi@mail.com"  -> cocok ✅                        │
│   "andi@@mail"     -> tidak cocok ❌                  │
└─────────────────────────────────────────────────────┘

Syntax Dasar Regex

Di PHP, pola regex ditulis di antara dua delimiter — biasanya garis miring /.../ — diikuti modifier opsional seperti i (case-insensitive).

php
<?php

$pattern = "/hello/i";  // Case-insensitive
$text = "Hello World";

if (preg_match($pattern, $text)) {
    echo "Found!";
}

// Metacharacters
// .  - Any character
// \d - Digit (0-9)
// \w - Word character (a-z, A-Z, 0-9, _)
// \s - Whitespace

// Quantifiers
// *  - 0 or more
// +  - 1 or more
// ?  - 0 or 1
// {n} - Exactly n
// {n,} - n or more
// {n,m} - Between n and m

?>

Karakter Khusus (Metacharacters)

PolaArtiCocok dengan
.Karakter apa puna, 5, _, spasi (bukan newline)
\dDigit0 sampai 9
\wWord charactera-z, A-Z, 0-9, _
\sWhitespacespasi, tab, newline
^Awal stringposisi awal
$Akhir stringposisi akhir
[abc]Salah satu karaktera, b, atau c
[a-z]Rentanghuruf kecil a sampai z
[^abc]Negasibukan a, b, c
\bWord boundarybatas kata

Quantifiers (Jumlah Kemunculan)

QuantifierArtiContoh
*0 atau lebiha* cocok "", "a", "aaa"
+1 atau lebiha+ cocok "a", "aaa" (bukan "")
?0 atau 1a? cocok "", "a"
{n}Tepat n kali\d{4} cocok "1234"
{n,}Minimal n kali\d{2,} cocok "12", "12345"
{n,m}Antara n dan m\d{2,4} cocok "12", "1234"
php
<?php

$pola = "/\d{4}-\d{4}-\d{4}/";  // 4 digit - 4 digit - 4 digit
$teks = "Hubungi 0812-3456-7890 ya!";

if (preg_match($pola, $teks)) {
    echo "Nomor ketemu";  // Output: Nomor ketemu
}

?>
💡Tip

^ dan $ sangat penting untuk validasi penuh. Tanpa keduanya, preg_match("/\d{4}/", "abc12345") tetap true karena "1234" ada di tengah string. Dengan ^...$, seluruh string harus cocok.

preg_match()

php
<?php

$email = "user@example.com";

// Validate email pattern
$pattern = "/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/";

if (preg_match($pattern, $email)) {
    echo "Valid email!";
}

// Get matches
$text = "Call 0812-3456-7890";
$pattern = "/\d{4}-\d{4}-\d{4}/";
preg_match($pattern, $text, $matches);
print_r($matches);  // [0812-3456-7890]

// All matches
preg_match_all($pattern, $text, $all);
print_r($all[0]);

?>

Perbedaan preg_match vs preg_match_all:

FungsiPerilakuKapan
preg_matchBerhenti di kecocokan pertamaCek validasi
preg_match_allKumpulkan semua kecocokanEkstraksi data

Capture Groups (Kurung)

Bagian pola yang dikurung (...) menjadi capture group — bisa diambil terpisah dari hasil:

php
<?php

$tanggal = "Lahir: 12-08-2026";

// Tangkap hari, bulan, tahun secara terpisah
$pola = "/(\d{2})-(\d{2})-(\d{4})/";
preg_match($pola, $tanggal, $m);

echo $m[0];  // 12-08-2026 (match penuh)
echo $m[1];  // 12 (hari)
echo $m[2];  // 08 (bulan)
echo $m[3];  // 2026 (tahun)

// Named groups (PHP 7.2+): beri nama, lebih jelas
$pola = "/(?<hari>\d{2})-(?<bulan>\d{2})-(?<tahun>\d{4})/";
preg_match($pola, $tanggal, $m2);

echo $m2["hari"];   // 12
echo $m2["tahun"];  // 2026

?>
💡Tip

Gunakan named groups (?<nama>...) untuk pola yang kompleks — hasilnya jauh lebih mudah dibaca daripada $m[1], $m[2], $m[3].

preg_match_all untuk Ekstraksi

php
<?php

$artikel = "Kontak: a@mail.com atau b@mail.com atau c@mail.com";

$pola = "/[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}/";
preg_match_all($pola, $artikel, $email_terambil);

print_r($email_terambil[0]);
// Output:
// Array
// (
//     [0] => a@mail.com
//     [1] => b@mail.com
//     [2] => c@mail.com
// )

echo "Ditemukan " . count($email_terambil[0]) . " email";
// Output: Ditemukan 3 email

?>

preg_replace()

php
<?php

$text = "Hello World";

// Replace
$new = preg_replace("/World/", "PHP", $text);
echo $new;  // Hello PHP

// Replace with regex
$phone = "0812-3456-7890";
$clean = preg_replace("/\D/", "", $phone);  // Hapus non-digit
echo $clean;  // 081234567890

// Callback replace
$numbers = [1, 2, 3, 4, 5];
$result = preg_replace_callback("/\d/", function($match) {
    return $match[0] * 2;
}, $numbers);

print_r($result);
// Output: [2, 4, 6, 8, 10]

?>

Backreference di Penggantian

$1, $2, dst. merujuk ke capture group dari pola — memungkinkan "menulis ulang" format:

php
<?php

$tanggal_amerika = "2026-08-12";

// Ubah format YYYY-MM-DD menjadi DD/MM/YYYY
$pola = "/(\d{4})-(\d{2})-(\d{2})/";
$hasil = preg_replace($pola, "$3/$2/$1", $tanggal_amerika);

echo $hasil;  // 12/08/2026

// Contoh lain: sensor kata kasar (case-insensitive)
$komentar = "Kamu BODOH banget!";
$bersih = preg_replace("/bodoh/i", "***", $komentar);
echo $bersih;  // Kamu *** banget!

?>
⚠️Perhatian

Dalam string double quotes, $1 bisa salah diinterpretasi sebagai variabel PHP. Tulis pola pengganti dalam single quotes ('$3/$2/$1') agar aman.

preg_split — Memecah String dengan Pola

Saudara explode, tapi dengan regex:

php
<?php

// Pecah berdasarkan satu atau lebih spasi/koma
$teks = "apel, pisang  jeruk,   mangga";
$buah = preg_split("/[\s,]+/", $teks);

print_r($buah);
// Output: ["apel", "pisang", "jeruk", "mangga"]

// Dengan explode biasa, spasi ganda jadi elemen kosong:
$buah2 = explode(" ", $teks);
print_r($buah2);
// Output: ["apel,", "pisang", "", "jeruk,", "", "", "mangga"]

?>

Common Patterns

php
<?php

// Phone number (Indonesian)
$phone = "/^08[1-9]\d{7,10}$/";

// URL
$url = "/https?:\/\/[\w\-.]+(\/[\w\-.?\/%&=]*)?/";

// Username (alphanumeric, 3-16 chars)
$username = "/^[a-zA-Z0-9_]{3,16}$/";

// Password (min 8 chars, 1 uppercase, 1 lowercase, 1 number)
$password = "/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/";

// Contoh penggunaan
$tests = [
    "081234567890" => $phone,
    "081234"       => $phone,       // terlalu pendek
    "andi_123"     => $username,
    "a!"           => $username,    // terlalu pendek & simbol
];

foreach ($tests as $nilai => $pola) {
    echo "$nilai => " . (preg_match($pola, $nilai) ? "valid" : "tidak valid") . "\n";
}

?>

Penjelasan pola password ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$:

BagianArti
(?=.*[a-z])Lookahead: di suatu tempat ada huruf kecil
(?=.*[A-Z])Lookahead: ada huruf besar
(?=.*\d)Lookahead: ada digit
.{8,}Total minimal 8 karakter apa pun
^...$Harus mencakup seluruh string

preg_quote — Meng-escape Otomatis

Saat user mengetik kata kunci pencarian yang kamu jadikan regex, karakter khusus di inputnya bisa merusak pola atau malah jadi "regex injection". Gunakan preg_quote:

php
<?php

$kata_kunci = "harga (promo)?";  // input user, penuh karakter regex

// ❌ Berbahaya: ( ) ? punya arti khusus di regex
// $pola = "/" . $kata_kunci . "/i";

// ✅ Aman: karakter khusus di-escape
$pola = "/" . preg_quote($kata_kunci, "/") . "/i";

echo $pola;  // /harga\ \(promo\)\?/i

if (preg_match($pola, "Promo hari ini: harga (promo)? serbu!")) {
    echo "Ketemu!";  // Output: Ketemu!
}

?>
⚠️Perhatian

Selalu preg_quote input user yang disisipkan ke pola regex — kalau tidak, karakter seperti (, ), [, * bisa mengubah arti pola dan menimbulkan bug atau bahkan serangan ReDoS.

Common Mistakes

Mistake 1: Lupa anchor ^ dan $ saat validasi

php
<?php

$kode = "AB12";

// ❌ Tanpa anchor: "XAB12Y" juga lolos!
if (preg_match("/[A-Z]{2}\d{2}/", "XAB12Y")) {
    echo "lolos";  // ❌ padahal format salah
}

// ✅ Dengan anchor: seluruh string harus cocok
if (preg_match("/^[A-Z]{2}\d{2}$/", "XAB12Y")) {
    echo "lolos";
} else {
    echo "ditolak";  // Output: ditolak
}

?>

Mistake 2: Tidak mengecek hasil preg_*

php
<?php

// preg_match mengembalikan:
//   1  = cocok
//   0  = tidak cocok
//   false = ERROR (pola tidak valid)

$pola = "/[a-z/";  // kurung tidak ditutup!

$hasil = preg_match($pola, "abc");
var_dump($hasil);  // Output: bool(false) — error, bukan "tidak cocok"!

// Bandingkan dengan cermat kalau perlu:
if ($hasil === false) {
    echo "Pola regex error: " . preg_last_error_msg();
}

?>
💡Tip

Jika preg_match tiba-tiba selalu false padahal polanya benar, cek error dengan preg_last_error() / preg_last_error_msg(). Pola yang tidak valid menghasilkan false, bukan 0.

Mistake 3: Regex katastropik (ReDoS)

php
<?php

// ❌ BERBAHAYA: nested quantifier (a+)* bisa meledak
// pada input panjang yang tidak cocok
// $pola = "/(a+)+$/";

// ✅ Hindari pola dengan quantifier bersarang seperti
// (a+)+, (a*)*, (a|a)+ pada input yang tidak terkendali.
// Jika harus, tambahkan batasan: (a{1,100})+ 

?>
⚠️Perhatian

ReDoS (Regular expression Denial of Service): pola seperti (a+)+ atau (a|a)* bisa membuat CPU bekerja eksponensial pada input panjang yang tidak cocok. Jangan pernah meng-compile regex dari input user, dan hindari nested quantifier.

Mistake 4: Lupa delimiter saat pola mengandung /

php
<?php

// ❌ Error: delimiter "/" bertabrakan dengan slash di pola
// $pola = "/https?:\/\/.../"  -> perlu escape atau ganti delimiter

// ✅ Ganti delimiter dengan # atau ~
$pola = "#https?://[\w.\-/]+#";
if (preg_match($pola, "https://marikoding.com")) {
    echo "URL valid";  // Output: URL valid
}

?>

Pro Tips

  1. Mulai dari pola sederhana lalu tambah kompleksitas bertahap — jangan tulis pola raksasa sekali jadi.
  2. Gunakan delimiter selain / (#, ~) saat pola berisi banyak slash (URL, path).
  3. Test pola di regex101.com atau sejenisnya sebelum dipakai di kode.
  4. Named groups (?<nama>...) untuk pola kompleks.
  5. preg_quote semua input user yang masuk ke pola.
  6. Regex itu senjata terakhir — untuk kasus sederhana, str_contains/str_replace lebih cepat dan lebih mudah dibaca.

Exercise

  1. Validasi format tanggal DD/MM/YYYY
  2. Extract semua email dari text
  3. Replace semua SPACES menjadi dash
  4. Tulis pola yang cocok dengan username: huruf/angka/underscore, 3-16 karakter, diawali huruf (bukan angka).
  5. Gunakan preg_replace dengan backreference untuk mengubah "John Doe" menjadi "Doe, John".
  6. Validasi password dengan preg_match dan tampilkan pesan error spesifik: terlalu pendek, kurang huruf besar, kurang angka.

Summary

KonsepPoin Kunci
DelimiterPola diapit /.../ atau #...#, modifier i = case-insensitive
Metachar. \d \w \s ^ $ [ ]
Quantifier* + ? {n} {n,} {n,m}
preg_matchCek kecocokan pertama (validasi)
preg_match_allAmbil semua kecocokan (ekstraksi)
preg_replaceGanti dengan pola + backreference $1
preg_splitPecah string dengan pola
Capture group(...) dan (?<nama>...)
Keamananpreg_quote input user, hindari nested quantifier

Klik tombol ini setelah menyelesaikan materi

📝

Quiz Bab 7

Uji pemahamanmu tentang String Manipulation