Aplikasi nyata jarang mengirim satu jenis email; ada verifikasi akun, reset password, nota pesanan, pengingat, dan puluhan lainnya. Bila isi tiap email disalin mentah ke dalam kode, teks berubah harus lewat edit kode, terjemahan jadi repot, dan kesalahan placeholder sulit dilacak. Template memisahkan isi dari logika: kode hanya menyiapkan data, sementara teks pesan hidup di cetakan tersendiri. Chapter ini membangun renderer template sederhana, menatanya dalam berkas, lalu mengamankan data pengguna yang masuk ke dalamnya.

Template: Satu Cetakan, Banyak Isi #

Inti template email adalah string berisi pengenal placeholder yang diganti nilai saat render. Mari kita mulai dari renderer paling sederhana yang bisa dipraktikkan:

php
<?php
function renderTemplate(string $template, array $data): string
{
    $hasil = $template;
    foreach ($data as $kunci => $nilai) {
        $hasil = str_replace("{{" . $kunci . "}}", (string) $nilai, $hasil);
    }

    return $hasil;
}

$template = "Halo {{nama}}, kode verifikasi Anda adalah {{kode}}. Kode ini berlaku 10 menit.";

echo renderTemplate($template, ["nama" => "Budi", "kode" => "123456"]) . "\n";

Output yang Dihasilkan:

text
Halo Budi, kode verifikasi Anda adalah 123456. Kode ini berlaku 10 menit.

Fungsi renderTemplate() bekerja sederhana: untuk setiap pasangan kunci-nilai dari $data, fungsi mengganti kemunculan {{kunci}} dengan nilainya memakai str_replace(). Sintaks kurung ganda {{nama}} dipilih karena jarang muncul di teks wajar dan mudah dibaca — mesin template besar seperti Twig memakai bentuk yang serupa, sehingga kebiasaan ini mudah dimigrasikan kelak.

Mendeteksi Placeholder yang Terlewat #

Ketika data yang tersedia tidak lengkap, placeholder yang tidak terganti akan lolos ke penerima — kata {{nomor}} nyaring tercetak di email pelanggan. Deteksi dini mencegah hal itu:

php
<?php
function placeholderTersisa(string $teks): array
{
    preg_match_all('/\{\{\s*(\w+)\s*\}\}/', $teks, $cocok);

    return array_values(array_unique($cocok[1]));
}

$hasil = "Halo Budi, pesanan {{nomor}} Anda dikirim ke {{kota}}. Pesanan {{nomor}} diperkirakan tiba besok.";

echo "Placeholder belum terisi: " . json_encode(placeholderTersisa($hasil), JSON_UNESCAPED_SLASHES) . "\n";

Output yang Dihasilkan:

text
Placeholder belum terisi: ["nomor","kota"]

Ekspresi reguler \{\{\s*(\w+)\s*\}\} menangkap kata di antara kurung ganda sambil memaafkan spasi seperti {{ nama }}. Hasil preg_match_all() lalu disaring array_unique() sehingga nomor yang muncul dua kali hanya dilaporkan sekali. Dalam praktik, fungsi seperti ini dipanggil setelah render: bila daftar hasil tidak kosong, pengiriman dibatalkan dan kekurangan data diperbaiki.

Menyimpan dan Menata Template #

Template yang tertanam di kode cepat menjadi sarang string panjang. Praktik yang rapi adalah menyimpannya sebagai berkas di dalam proyek:

text
templates/
└── email/
    ├── verifikasi/
    │   ├── id.txt
    │   └── id.html
    └── reset-password/
        ├── id.txt
        └── id.html

Satu folder per jenis email, dua berkas per versi isi — txt untuk teks murni dan html untuk tampilan kaya — sesuai struktur multipart/alternative pada chapter sebelumnya. Akhiran id adalah kode bahasa; menambah en.txt dan en.html kapan pun dibutuhkan menjadi pintu terjemahan tanpa menyentuh kode. Karena template hidup di repositori yang sama dengan kode, perubahan teks ikut terlacak di riwayat Git dan bisa direview lewat pull request — inilah cara versioning paling murah dan transparan.

Berikut cara memuat dan merender template dari berkas:

php
<?php
function renderTemplate(string $template, array $data): string
{
    $hasil = $template;
    foreach ($data as $kunci => $nilai) {
        $hasil = str_replace("{{" . $kunci . "}}", (string) $nilai, $hasil);
    }

    return $hasil;
}

$folder = sys_get_temp_dir() . "/latihan-template";
if (!is_dir($folder)) {
    mkdir($folder);
}

file_put_contents($folder . "/verifikasi.txt", "Halo {{nama}}, kode verifikasi Anda adalah {{kode}}.");

$isiTemplate = file_get_contents($folder . "/verifikasi.txt");
echo renderTemplate($isiTemplate, ["nama" => "Siti", "kode" => "654321"]) . "\n";

unlink($folder . "/verifikasi.txt");
rmdir($folder);

Output yang Dihasilkan:

text
Halo Siti, kode verifikasi Anda adalah 654321.

Latihan ini menulis berkas template ke folder sementara sistem agar bisa dijalankan tanpa menyentuh isi situs materi. Pada proyek nyata, sys_get_temp_dir() diganti path ke folder templates/email/ proyek Anda, dan nama berkas dibangun dari jenis email ditambah locale, misalnya verifikasi/id.txt. Alur muat-isi-sajikan ini persis yang kemudian diserahkan ke library seperti Twig atau Mustache bila kebutuhan bertumbuh.

Dua Versi Isi dalam Satu Struktur #

Renderer menjadi lebih berguna ketika hasilnya berupa struktur lengkap — subjek dan dua versi isi — siap diserahkan ke library email:

php
<?php
function amankanHtml(string $nilai): string
{
    return htmlspecialchars($nilai, ENT_QUOTES, "UTF-8");
}

function renderEmailVerifikasi(array $data): array
{
    $namaAman = amankanHtml($data["nama"]);

    return [
        "subject" => "Kode Verifikasi Akun Anda",
        "text" => "Halo {$data["nama"]}, kode verifikasi Anda: {$data["kode"]}",
        "html" => "<p>Halo $namaAman, kode verifikasi Anda: <strong>{$data["kode"]}</strong></p>",
    ];
}

$email = renderEmailVerifikasi([
    "nama" => "Budi <[email protected]>",
    "kode" => "123456",
]);

foreach ($email as $bagian => $isi) {
    echo "== $bagian ==\n$isi\n";
}

Output yang Dihasilkan:

text
== subject ==
Kode Verifikasi Akun Anda
== text ==
Halo Budi <[email protected]>, kode verifikasi Anda: 123456
== html ==
<p>Halo Budi &lt;[email protected]&gt;, kode verifikasi Anda: <strong>123456</strong></p>

Fungsi renderEmailVerifikasi() mengembalikan array berisi subject, text, dan html — bentuk yang langsung bisa dipetakan ke text() dan html() pada Symfony Mailer atau AltBody pada PHPMailer. Perhatikan pembagian tugas di dalamnya: subject bebas dari data pengguna sehingga aman, text menerima data mentah karena teks murni tidak dieksekusi, dan html memakai $namaAman yang sudah disaring. Nilai nama pada contoh sengaja berisi tanda < agar kita bisa melihat efek penyaringannya pada bagian html.

Mengamankan Data Pengguna #

Data pengguna yang masuk ke email membawa dua risiko berbeda. Risiko pertama menyasar bagian HTML: nilai seperti nama bisa menyelundupkan tag asing. Fungsi amankanHtml() di atas menanganinya dengan htmlspecialchars() — tanda <, >, " menjadi entitas aman, sehingga mereka tampil sebagai teks, bukan markup.

Risiko kedua lebih senyap dan menyasar header. Karakter baris baru pada nilai yang disisipkan ke header bisa “menyembunyikan” header tambahan palsu:

php
<?php
function amankanHeader(string $nilai): string
{
    return str_replace(["\r", "\n"], "", trim($nilai));
}

$subjek = "Halo\r\nBcc: [email protected]";

echo "Subject: " . amankanHeader($subjek) . "\n";

Output yang Dihasilkan:

text
Subject: HaloBcc: [email protected]

Nilai $subjek di atas mencoba menyelipkan header Bcc untuk menambah penerima tersembunyi. Fungsi amankanHeader() membuang setiap \r dan \n sehingga serangan ambruk menjadi teks sambung yang tidak membentuk header baru. Terapkan fungsi ini pada setiap nilai yang menempel di header — termasuk nama tampilan pada From dan To — dan jangan pernah menerima subjek mentah dari input pengguna.

Ringkasan #

Template memisahkan isi email dari logika kode: renderer str_replace() mengganti placeholder {{nama}}, placeholderTersisa() menjaga kualitas data, berkas per jenis per locale menjaga organisasi dan terjemahan, dan struktur subject-text-html menjembatani langsung ke library email. Penyaringan amankanHtml() dan amankanHeader() menutup celah data pengguna di kedua bagian pesan. Dengan isi yang rapi dan aman, tantangan berikutnya adalah ketahanan pengiriman — queueing, retry, dan penanganan kegagalan delivery yang akan dibahas pada chapter lanjutan.

Referensi #