PHP tidak pernah mengeluh pada fungsi yang buruk: selama sintaksnya valid, program berjalan dan pengguna tidak melihat bedanya. Kualitas fungsi justru terasa ketika program tumbuh — fungsi yang tadinya dua puluh baris membesar menjadi dua ratus, satu perubahan kecil merusak tiga fitur lain, dan anggota tim baru butuh sehari hanya untuk memahami satu pemanggilan. Fungsi yang baik mudah dibaca tiga bulan kemudian, mudah diuji secara terpisah, dan aman diubah. Pada Chapter penutup Section ini kita merangkum prinsip praktis perancangan fungsi — tanggung jawab tunggal, penamaan, desain parameter, efek samping, nilai kembali, dan dokumentasi — disertai contoh refactor sebelum dan sesudah.

Satu Fungsi Satu Tanggung Jawab #

Aturan paling berpengaruh: satu fungsi menyelesaikan satu tugas. Fungsi yang menanggung banyak tugas sulit diberi nama yang jujur, sulit diuji tanpa menyiapkan lingkungan lengkap, dan setiap perubahan kecil di dalamnya berisiko merusak tugas lain yang kebetulan menumpang. Perhatikan contoh berikut yang terasa wajar di proyek nyata:

php
<?php
// Satu fungsi, tiga tanggung jawab sekaligus
function prosesPesanan(array $pesanan): string
{
    // Tanggung jawab 1: menghitung total belanja
    $total = 0;
    foreach ($pesanan as $item) {
        $total += $item['harga'] * $item['jumlah'];
    }

    // Tanggung jawab 2: menerapkan aturan diskon
    if ($total >= 100000) {
        $total = $total * 0.9;
    }

    // Tanggung jawab 3: mencetak dan mencatat ke berkas
    echo 'Total: Rp' . $total . "\n";
    file_put_contents('pesanan.log', "total=$total\n", FILE_APPEND);

    return 'OK';
}

Fungsi di atas bekerja, tetapi mencoba menguji aturan diskonnya berarti harus menangkap keluaran echo, menyediakan berkas pesanan.log, dan mengabaikan hasil 'OK'. Refactor berikut memecahnya menjadi fungsi-fungsi kecil yang masing-masing bisa dipanggil dan diuji sendiri:

php
<?php
/**
 * Menghitung total harga sekumpulan item pesanan.
 *
 * @param array<int, array{nama: string, harga: int, jumlah: int}> $pesanan
 * @return int Total harga dalam satuan Rupiah.
 */
function hitungTotalPesanan(array $pesanan): int
{
    $total = 0;
    foreach ($pesanan as $item) {
        $total += $item['harga'] * $item['jumlah'];
    }

    return $total;
}

function hitungPersentaseDiskon(int $total): float
{
    return $total >= 100000 ? 0.1 : 0.0;
}

function totalSetelahDiskon(array $pesanan): float
{
    $total = hitungTotalPesanan($pesanan);

    return $total * (1 - hitungPersentaseDiskon($total));
}

$pesanan = [
    ['nama' => 'Kopi', 'harga' => 25000, 'jumlah' => 3],
    ['nama' => 'Teh', 'harga' => 8000, 'jumlah' => 5],
];

echo 'Total: Rp' . hitungTotalPesanan($pesanan) . "\n";
echo 'Setelah diskon: Rp' . totalSetelahDiskon($pesanan) . "\n";

Output yang Dihasilkan:

text
Total: Rp115000
Setelah diskon: Rp103500

Setelah dipecah, tiga hal membaik sekaligus. Mencetak dan mencatat kini menjadi urusan pemanggil, bukan bagian dari perhitungan — echo dan file_put_contents() pindah ke lapisan tampilan dan pencatatan. Kemudian hitungPersentaseDiskon() mengembalikan angka antara 0.0 dan 1.0 yang bisa diverifikasi dengan satu pemanggilan, bukan string pesan yang harus diurai. Terakhir, setiap fungsi cukup pendek untuk dibaca dari atas ke bawah tanpa bergulir layar.

Nama yang Menggambarkan Niat #

Nama fungsi adalah dokumentasi termurah: nama terbaca di setiap pemanggilan tanpa perlu membuka isi fungsi. Prinsipnya sederhana — fungsi melakukan aksi sehingga namanya berawalan kata kerja, fungsi yang menjawab pertanyaan ya/tidak berawalan is atau has, dan nama samar adalah bau bahaya karena bisa berarti apa saja:

Nama kurang jelasNama yang menggambarkan niat
prosesData($baris)normalisasiBaris($baris)
check($email)isValidEmail($email)
get($id)cariPenggunaById($id)

Nama panjang yang jujur hampir selalu lebih baik daripada nama pendek yang harus diperiksa isinya setiap kali dibaca. Kalau Anda kesulitan memberi nama, sering kali itu bukan soal kosa kata — melainkan sinyal bahwa fungsi sedang mengerjakan lebih dari satu hal dan layak dipecah.

Parameter yang Mudah Dipakai Benar #

Parameter adalah antarmuka publik fungsi; tanda tangan yang baik membuat pemakaian salah hampir mustahil. Dua aturan praktis paling berguna: hindari parameter boolean yang mengubah perilaku, dan jaga jumlah parameter tetap kecil. Boolean perilaku adalah tanda dua fungsi berbeda menyamar menjadi satu:

php
<?php
// Pemanggil harus membuka isi fungsi untuk tahu arti `true`
function buatTagNama(array $pengguna, bool $denganGelar): string
{
    return $denganGelar
        ? $pengguna['gelar'] . ' ' . $pengguna['nama']
        : $pengguna['nama'];
}

Pemanggilan buatTagNama($pengguna, true) tidak mengatakan apa-apa tentang makna true — pembaca harus menebak atau membuka definisinya. Pemecahan yang bersih adalah memisahkan kedua perilaku menjadi dua fungsi bernama:

php
<?php
function namaPendek(array $pengguna): string
{
    return $pengguna['nama'];
}

function namaLengkap(array $pengguna): string
{
    return $pengguna['gelar'] . ' ' . $pengguna['nama'];
}

$pengguna = ['nama' => 'Budi Santoso', 'gelar' => 'Dr.'];

echo namaPendek($pengguna) . "\n";
echo namaLengkap($pengguna) . "\n";

Output yang Dihasilkan:

text
Budi Santoso
Dr. Budi Santoso

Sekarang setiap pemanggilan menjadi kalimat yang terbaca: maksud namaLengkap($pengguna) jelas tanpa satu pun argumen tambahan. Untuk menjaga jumlah parameter tetap kecil, manfaatkan nilai bawaan untuk parameter opsional (Chapter Default Parameters ), named arguments agar pemanggilan panjang tetap jelas (Chapter Named Arguments ), dan variadic functions saat jumlah argumen memang tidak pasti (Chapter Variadic Functions ).

Efek Samping dan Fungsi Murni #

Fungsi murni (pure function) hanya menghitung hasil dari inputnya dan tidak mengubah apa pun di luar dirinya: tidak menyentuh variabel global, tidak menulis berkas, tidak mencetak. Kemurnian membuat fungsi mudah diuji — panggil dengan input yang sama, hasilnya pasti sama — dan aman dipanggil dari mana saja. PHP membantu di sini karena array dan nilai skalar disalin saat dikirim ke fungsi; efek samping pada data pemanggil hanya muncul bila kita memintanya secara eksplisit dengan &. Perbandingan berikut memperlihatkan keduanya:

php
<?php
// Efek samping: mengubah data pemanggil lewat parameter referensi
function tandaiSemuaSelesai(array &$tugas): void
{
    foreach ($tugas as &$tugasItem) {
        $tugasItem['selesai'] = true;
    }
}

// Murni: mengembalikan data baru, data asli tidak disentuh
function buatDaftarSelesai(array $tugas): array
{
    $hasil = [];
    foreach ($tugas as $tugasItem) {
        $tugasItem['selesai'] = true;
        $hasil[] = $tugasItem;
    }

    return $hasil;
}

$tugasAsli = [
    ['judul' => 'Belajar PHP', 'selesai' => false],
    ['judul' => 'Kirim laporan', 'selesai' => false],
];

$hasilMurni = buatDaftarSelesai($tugasAsli);
echo 'Setelah fungsi murni: ' . ($tugasAsli[0]['selesai'] ? 'selesai' : 'belum selesai') . "\n";
echo 'Hasil fungsi murni: ' . ($hasilMurni[0]['selesai'] ? 'selesai' : 'belum selesai') . "\n";

tandaiSemuaSelesai($tugasAsli);
echo 'Setelah fungsi efek samping: ' . ($tugasAsli[0]['selesai'] ? 'selesai' : 'belum selesai') . "\n";

Output yang Dihasilkan:

text
Setelah fungsi murni: belum selesai
Hasil fungsi murni: selesai
Setelah fungsi efek samping: selesai

Dua baris hasil pertama membuktikan buatDaftarSelesai() tidak mengubah $tugasAsli: data asli masih belum selesai meski hasil fungsinya sudah selesai. Baris ketiga memperlihatkan perilaku sebaliknya — setelah tandaiSemuaSelesai() dipanggil, data asli berubah menjadi selesai. Uji cepat yang bisa Anda pakai: apakah memanggil fungsi dua kali dengan input yang sama selalu memberikan hasil yang sama tanpa jejak di tempat lain? Bila ya, fungsi tersebut murni. Fungsi ber efek samping tetap dibutuhkan — program pada akhirnya harus mencetak dan menyimpan — namun semakin banyak logika inti yang murni, semakin mudah program diuji dan dirawat.

Nilai Kembali yang Konsisten #

Fungsi yang kadang mengembalikan array, kadang false, kadang string memaksa setiap pemanggilan menebak tipe hasil sebelum menggunakannya. Kembalian campuran seperti ini adalah warisan pola lama PHP dari era sebelum type declaration lazim. Bandingkan bentuk lamanya:

php
<?php
// Kembalian campuran: pemanggil harus menebak tipe hasil
function cariPenggunaLama(int $id)
{
    if ($id <= 0) {
        return false;               // boolean
    }

    if ($id === 1) {
        return ['nama' => 'Budi']; // array
    }

    return 'tidak ditemukan';       // string
}

Versi perbaikannya memilih satu kontrak: data pengguna berupa array, atau null bila tidak ada — dinyatakan eksplisit lewat return type ?array yang kita bahas tuntas pada Chapter Return Types :

php
<?php
function cariPengguna(int $id): ?array
{
    if ($id !== 1) {
        return null;
    }

    return ['nama' => 'Budi', 'email' => '[email protected]'];
}

$pengguna = cariPengguna(1);
echo $pengguna === null
    ? "Pengguna tidak ditemukan\n"
    : "Halo, {$pengguna['nama']} <{$pengguna['email']}>\n";

$pengguna = cariPengguna(99);
echo $pengguna === null
    ? "Pengguna tidak ditemukan\n"
    : "Halo, {$pengguna['nama']}\n";

Output yang Dihasilkan:

text
Halo, Budi <[email protected]>
Pengguna tidak ditemukan

Dengan kontrak ?array, seluruh pemanggilan cukup satu jenis pemeriksaan — $pengguna === null — dan alat analisis dapat memverifikasi setiap pemakaian variabel hasil. Prinsipnya: pilih tipe kembalian sesempit mungkin lalu konsisten; simpan union type seperti int|string untuk kasus yang memang bermakna ganda, bukan sebagai pintu darurat.

Alur Rata dengan Return Early #

Kita sudah melihat guard clause pada Chapter Return Values ; dalam konteks perancangan, pola ini layak menjadi kebiasaan default. Kondisi penolak diperiksa di awal lalu return segera keluar, sehingga sisa tubuh fungsi hanya berisi alur utama tanpa menjorok dalam-dalam. Berikut bentuk yang menjorok tiga tingkat:

php
<?php
// Kondisi aktif sulit dilacak karena menjorok dalam-dalam
function kirimInvoiceBersarang(array $pesanan): void
{
    if ($pesanan !== []) {
        if (isset($pesanan[0]['email'])) {
            $email = $pesanan[0]['email'];
            if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
                echo "Invoice dikirim ke $email\n";
            }
        }
    }
}

Dan berikut versi rata yang menolak lebih dulu:

php
<?php
function kirimInvoice(array $pesanan): void
{
    if ($pesanan === []) {
        return;
    }

    $email = $pesanan[0]['email'] ?? null;
    if ($email === null || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return;
    }

    echo "Invoice dikirim ke $email\n";
}

kirimInvoice([['email' => '[email protected]']]);
kirimInvoice([['email' => 'bukan-email']]);
kirimInvoice([]);

Output yang Dihasilkan:

text
Invoice dikirim ke [email protected]

Fungsi kirimInvoice() memakai filter_var() dengan konstanta FILTER_VALIDATE_EMAIL untuk memeriksa format email — fungsi bawaan PHP yang akan kita pakai lagi saat membahas validasi form. Dari tiga pemanggilan di atas, hanya yang pertama lolos semua guard; dua lainnya berhenti diam tanpa mencetak apa pun. Versi rata terbaca seperti daftar syarat — pesanan tidak boleh kosong, email harus ada, email harus valid — dan baris terakhir tinggal mengerjakan tugas sebenarnya.

Docblock sebagai Kontrak #

Terakhir, ketika tanda tangan sendiri belum menceritakan semuanya — satuan, format hasil, prasyarat input — docblock /** */ mengisi celah tersebut dan sekaligus terbaca oleh IDE serta alat analisis:

php
<?php
/**
 * Memformat durasi detik menjadi teks ringkas.
 *
 * @param int $detik Jumlah detik, harus >= 0.
 * @return string Contoh hasil: "1 jam 30 menit".
 */
function formatDurasi(int $detik): string
{
    $jam = intdiv($detik, 3600);
    $menit = intdiv($detik % 3600, 60);

    if ($jam === 0) {
        return "$menit menit";
    }

    return "$jam jam $menit menit";
}

echo formatDurasi(5400) . "\n";
echo formatDurasi(2700) . "\n";

Output yang Dihasilkan:

text
1 jam 30 menit
45 menit

Satuan adalah contoh klasik informasi yang tidak tampak dari int: tanpa docblock, pembaca bisa mengira formatDurasi(5400) menerima menit. Anotasi @param dan @return juga membantu PHPStan dan Psalm menangkap kesalahan tipe sebelum program berjalan.

Ringkasan #

Fungsi yang baik menanggung satu tanggung jawab, bernama sesuai niatnya, menerima parameter sedikit dan bebas boolean perilaku, menghindari efek samping bila tidak diminta, mengembalikan satu tipe yang konsisten, memakai return early agar alurnya rata, dan mendokumentasikan kontraknya lewat docblock. Prinsip-prinsip ini tidak berhenti di fungsi — metodologi yang sama berlaku untuk method pada class yang akan kita temui di Section Object-Oriented Programming, dan menjadi fondasi standar penulisan kode yang dibahas lebih jauh di Section Coding Standards dan Maintainability.

Referensi #