Bagian 2 meninggalkan kalkulator yang cepat saat input benar, tetapi jatuh tersungkur saat input salah: operator asing memicu fatal error, intdiv menerima keluhan TypeError dari float, dan modulo diam-diam memotong pecahan. Pada bagian ini kita memasang lapisan validasi di gerbang masuk — sebelum satu pun perhitungan dijalankan. Prinsip kerjanya sederhana: input pengguna dianggap tidak dipercaya sampai terbukti valid, dan penolakan disampaikan dengan pesan yang jelas plus exit code 1, bukan dengan crash yang menakutkan. Setelah bagian ini selesai, satu-satunya cara program masih bisa jatuh adalah pembagian dengan nol — kejadian yang sengaja kita simpan untuk Bagian 4.

Prinsip: Validasi di Gerbang Masuk #

Validasi paling efektif dilakukan di batas antara dunia luar dan logika program — di sini berarti di commandHitung(), sebelum fungsi hitung() dipanggil. Ada dua manfaat sekaligus. Pertama, fungsi hitung() boleh tetap ramping karena tidak perlu menganggap semua masukannya curiga; ia cukup percaya bahwa pemanggilnya sudah menyaring. Kedua, pesan error dirumuskan di satu tempat sehingga konsisten — pengguna selalu melihat format yang sama, dan kita selalu tahu ke mana harus mencari ketika pesan perlu diperbaiki.

Kebutuhan validasi hitung terbagi dua gerbang. Gerbang pertama menjawab: apakah bentuk panggilannya benar, yaitu jumlah argumennya tepat tiga? Gerbang kedua menjawab: apakah isi ketiga argumen itu benar, yaitu keduanya angka dan operatornya dikenal?

Gerbang Pertama: Jumlah Argumen #

Pemanggilan seperti php main.php hitung 5 + (operand kedua kosong) atau php main.php hitung 5 + 3 9 (argumen berlebih) ditolak sebelum apa pun dibaca isinya. Berikut inti pemeriksaannya sebagai contoh yang berdiri sendiri — program berhenti dengan exit(1) setelah mencetak pesan:

php
<?php

function validasiJumlahArgumen(array $args): ?string
{
    if (count($args) !== 3) {
        return 'Error: perintah hitung membutuhkan tepat 3 argumen '
            . '(angka1 operator angka2), diberikan ' . count($args) . '.';
    }

    return null;
}

// Simulasi pemanggilan: php main.php hitung 5 +   (operand kedua kosong)
$argsSimulasi = ['5', '+'];

$error = validasiJumlahArgumen($argsSimulasi);
if ($error !== null) {
    echo $error . "\n";
    exit(1);
}

Output yang Dihasilkan:

text
Error: perintah hitung membutuhkan tepat 3 argumen (angka1 operator angka2), diberikan 2.

Pola ?string di sini adalah kontrak yang nyaman: nilai balik null berarti “tidak ada masalah”, sedangkan string berarti “ini pesan errornya”. Pemanggil tinggal memeriksa !== null untuk memutuskan lanjut atau berhenti — tidak perlu exception untuk kasus yang memang biasa terjadi pada program CLI. Dua catatan tentang versi sungguhan di main.php: pesan error dikirim lewat fwrite(STDERR, ...) alih-alih echo (keluaran error sepatutnya tidak bercampur dengan keluaran normal), dan setelah pesan tersebut program juga mencetak printUsage() karena pengguna yang salah bentuk paling butuh melihat bentuk yang benar.

Gerbang Kedua: Angka, Operator, dan Syarat Bilangan Bulat #

Setelah bentuk panggilan lolos, isi ketiga argumen diperiksa oleh fungsi validateOperands(). Pemeriksaan pertama tetap memakai is_numeric() untuk kedua operand — tabel perilakunya untuk berbagai bentuk string sudah kita lihat di Bagian 2. Pemeriksaan kedua memastikan operator termasuk daftar yang dikenali, dan di sini kita memakai in_array() mode strict:

php
<?php

$didukung = ['+', '-', '*', '/', '%', '**', 'intdiv'];

var_dump(in_array('+', $didukung, true));
var_dump(in_array('pangkat', $didukung, true));

Output yang Dihasilkan:

text
bool(true)
bool(false)

Parameter ketiga true menyalakan perbandingan strict (===) antar elemen. Untuk daftar operator string seperti ini perbedaannya memang tipis, tetapi kebiasaan strict mencegah kejutan ketika daftar itu kelak berisi campuran tipe. Daftar operator di main.php disimpan pada konstanta KNOWN_OPERATORS sehingga fungsi printUsage() dan validateOperands() membaca dari sumber yang sama — menambah operator cukup satu kali di satu tempat.

Pemeriksaan ketiga adalah yang paling spesifik: operator % dan intdiv hanya bermakna untuk bilangan bulat. Bagian 2 sudah memperlihatkan akibatnya bila aturan ini dilanggar — TypeError dari intdiv() dan hasil terpotong dari %. Karena is_numeric() menerima "7.5" sebagai angka yang sah, kita perlu pemeriksaan yang lebih tajam: apakah string tersebut bentuk harfiah bilangan bulat?

php
<?php

function isIntegerLiteral(string $value): bool
{
    return preg_match('/^[+-]?\d+$/', $value) === 1;
}

var_dump(isIntegerLiteral('7'), isIntegerLiteral('-3'), isIntegerLiteral('7.5'), isIntegerLiteral('1e3'));

Output yang Dihasilkan:

text
bool(true)
bool(true)
bool(false)
bool(false)

Pola regex ^[+-]?\d+$ hanya menerima tanda opsional diikuti deretan digit — "7" dan "-3" lolos, "7.5" dan "1e3" tertolak. Fungsi ini sekaligus dipakai ulang oleh toNumber() untuk memutuskan konversi ke int atau float, jadi definisi “bilangan bulat” di program ini tunggal dan konsisten. Bila salah satu operand gagal, validasi mengembalikan pesan seperti Error: operator '%' hanya menerima bilangan bulat, diberikan '7.5'.

Dua Gerbang di commandHitung #

Dengan semua alat di tangan, commandHitung() versi bagian ini berbentuk dua pemeriksaan berurutan lalu inti perhitungan yang kini bebas kecurigaan:

php
<?php

// fragmen commandHitung() — Bagian 3
function commandHitung(array $args): int
{
    // Gerbang pertama: bentuk panggilan (jumlah argumen).
    if (count($args) !== 3) {
        fwrite(STDERR, "Error: perintah hitung membutuhkan tepat 3 argumen "
            . '(angka1 operator angka2), diberikan ' . count($args) . ".\n\n");
        printUsage();

        return 1;
    }

    // Gerbang kedua: isi setiap argumen.
    $error = validateOperands($args);
    if ($error !== null) {
        fwrite(STDERR, $error . "\n");

        return 1;
    }

    [$aRaw, $operator, $bRaw] = $args;
    $hasil = hitung(toNumber($aRaw), $operator, toNumber($bRaw));

    echo $hasil . "\n";

    return 0;
}

Blok di atas adalah fragmen — ia menyebut fungsi validateOperands() yang definisinya ada di berkas main.php pada bagian bawah halaman. Perhatikan pula bahwa trik array_pad() dari Bagian 2 sudah tidak diperlukan: setelah jumlah argumen dipastikan tepat tiga, destrukturisasi [$aRaw, $operator, $bRaw] = $args aman tanpa peringatan apa pun.

Menjalankan Uji Validasi #

Empat pemanggilan bermasalah dari Bagian 2 kini kita ajukan ulang — hasilnya kini pesan yang bisa dibaca manusia dan exit code 1 di semua kasus:

bash
cd content/php/proyek-php/kalkulator-terminal/bagian-3-validasi-input
php main.php hitung 5 +; echo "Exit code: $?"
text
Error: perintah hitung membutuhkan tepat 3 argumen (angka1 operator angka2), diberikan 2.

Pemakaian:
  php main.php hitung <angka1> <operator> <angka2>
  php main.php versi

Operasi yang didukung:
  +  -  *  /  %  **  intdiv
Exit code: 1
bash
php main.php hitung 5 pangkat 3; echo "Exit code: $?"
php main.php hitung abc + 1; echo "Exit code: $?"
php main.php hitung 7.5 % 2; echo "Exit code: $?"
text
Error: operator tidak dikenal: 'pangkat'. Operator yang didukung: + - * / % ** intdiv.
Exit code: 1
Error: 'abc' bukan angka yang valid.
Exit code: 1
Error: operator '%' hanya menerima bilangan bulat, diberikan '7.5'.
Exit code: 1

Satu kasus masih sengaja dibiarkan terbuka: php main.php hitung 10 / 0 masih berakhir sebagai Fatal error: Uncaught DivisionByZeroError dengan exit code 255. Pembagian dengan nol bukan kesalahan bentuk maupun isi argumen — angka 0 adalah operand yang sah — melainkan kejadian saat runtime yang tempat penanganannya adalah try-catch, materi yang menjadi jantung Bagian 4.

Apa yang Berubah dari Bagian 2 #

Perubahan bagian ini seluruhnya berada di lapisan pemeriksaan; fungsi hitung() dan toNumber() tidak berubah perilaku. Rinciannya:

  • commandHitung() memeriksa jumlah argumen lebih dulu; salah bentuk panggilan mendapat pesan error plus panduan pemakaian, exit code 1.
  • Fungsi baru validateOperands() memeriksa angka, operator, dan syarat bilangan bulat untuk % dan intdiv — mengembalikan ?string sebagai pesan error.
  • Fungsi baru isIntegerLiteral() menjadi satu-satunya definisi “bentuk bilangan bulat”, dipakai bersama oleh validateOperands() dan toNumber().
  • Konstanta baru KNOWN_OPERATORS menjadi sumber tunggal daftar operator untuk validasi dan panduan pemakaian.
  • APP_VERSION naik menjadi 0.3.0 — program kini menolak input salah dengan sopan, menyisakan satu kasus crash yang disengaja.

Referensi #

  • is_numeric — daftar bentuk string yang dianggap numerik.
  • in_array — perilaku mode strict pada parameter ketiga.
  • Numeric strings — perbedaan string numerik dan non-numerik di PHP 8.

Berkas Sumber

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

main.php

Unduh berkas
php
<?php

declare(strict_types=1);

/**
 * Kalkulator Terminal — Bagian 3: Validasi Input.
 *
 * Pemakaian:
 *   php main.php hitung <angka1> <operator> <angka2>
 *   php main.php versi
 *
 * Dipanggil tanpa argumen, program mencetak panduan pemakaian lalu
 * keluar dengan exit code 0.
 */

const APP_NAME = 'Kalkulator Terminal';
const APP_VERSION = '0.3.0';

/** Daftar operator yang dikenali oleh perintah hitung. */
const KNOWN_OPERATORS = ['+', '-', '*', '/', '%', '**', 'intdiv'];

/** Cetak panduan pemakaian program ke STDOUT. */
function printUsage(): void
{
    echo "Pemakaian:\n";
    echo "  php main.php hitung <angka1> <operator> <angka2>\n";
    echo "  php main.php versi\n";
    echo "\n";
    echo "Operasi yang didukung:\n";
    echo '  ' . implode('  ', KNOWN_OPERATORS) . "\n";
}

/** Tangani perintah `versi`: cetak nama dan versi program. */
function commandVersi(): int
{
    echo APP_NAME . ' versi ' . APP_VERSION . "\n";

    return 0;
}

/**
 * Ubah string argumen menjadi angka int atau float.
 *
 * Bentuk bilangan bulat ("5", "-3", "+7") dikonversi ke int; bentuk
 * lain yang lolos pemeriksaan is_numeric() ("7.5", "1e3") dikonversi
 * ke float supaya bagian pecahannya tidak hilang.
 */
function toNumber(string $value): int|float
{
    if (isIntegerLiteral($value)) {
        return (int) $value;
    }

    return (float) $value;
}

/** Apakah string ini bentuk harfiah bilangan bulat, mis. "5" atau "-3"? */
function isIntegerLiteral(string $value): bool
{
    return preg_match('/^[+-]?\d+$/', $value) === 1;
}

/**
 * Validasi isi tiga argumen perintah hitung.
 *
 * Mengembalikan string pesan error bila ada masalah, atau null bila
 * seluruh argumen lolos validasi.
 */
function validateOperands(array $args): ?string
{
    [$aRaw, $operator, $bRaw] = $args;

    foreach ([$aRaw, $bRaw] as $operand) {
        if (!is_numeric($operand)) {
            return "Error: '{$operand}' bukan angka yang valid.";
        }
    }

    if (!in_array($operator, KNOWN_OPERATORS, true)) {
        return "Error: operator tidak dikenal: '{$operator}'. "
            . 'Operator yang didukung: ' . implode(' ', KNOWN_OPERATORS) . '.';
    }

    if ($operator === '%' || $operator === 'intdiv') {
        foreach ([$aRaw, $bRaw] as $operand) {
            if (!isIntegerLiteral($operand)) {
                return "Error: operator '{$operator}' hanya menerima bilangan bulat, "
                    . "diberikan '{$operand}'.";
            }
        }
    }

    return null;
}

/**
 * Hitung satu operasi aritmatika dan kembalikan hasilnya.
 *
 * Operator yang dikenali: + - * / % ** intdiv. Pembagian dengan nol
 * masih melempar DivisionByZeroError; Bagian 4 akan menanganinya.
 */
function hitung(int|float $a, string $operator, int|float $b): int|float
{
    return match ($operator) {
        '+' => $a + $b,
        '-' => $a - $b,
        '*' => $a * $b,
        '/' => $a / $b,
        '%' => $a % $b,
        '**' => $a ** $b,
        'intdiv' => intdiv($a, $b),
    };
}

/** Tangani perintah `hitung <angka1> <operator> <angka2>`. */
function commandHitung(array $args): int
{
    // Gerbang pertama: bentuk panggilan (jumlah argumen).
    if (count($args) !== 3) {
        fwrite(STDERR, "Error: perintah hitung membutuhkan tepat 3 argumen "
            . '(angka1 operator angka2), diberikan ' . count($args) . ".\n\n");
        printUsage();

        return 1;
    }

    // Gerbang kedua: isi setiap argumen.
    $error = validateOperands($args);
    if ($error !== null) {
        fwrite(STDERR, $error . "\n");

        return 1;
    }

    [$aRaw, $operator, $bRaw] = $args;
    $a = toNumber($aRaw);
    $b = toNumber($bRaw);

    $hasil = hitung($a, $operator, $b);

    echo $hasil . "\n";

    return 0;
}

/** Laporkan perintah yang tidak dikenal, lalu cetak panduan pemakaian. */
function unknownCommand(string $command): int
{
    fwrite(STDERR, "Error: perintah tidak dikenal: {$command}\n");
    printUsage();

    return 1;
}

/**
 * Titik masuk program: terima $argv, tentukan perintah yang diminta,
 * dan kembalikan exit code ke pemanggil.
 */
function main(array $argv): int
{
    // Buang nama skrip (elemen pertama) — sisanya adalah argumen pengguna.
    $args = array_slice($argv, 1);

    // Tanpa argumen: pengguna hanya butuh tahu cara memakai program.
    if ($args === []) {
        printUsage();

        return 0;
    }

    $command = $args[0];
    $commandArgs = array_slice($args, 1);

    return match ($command) {
        'versi' => commandVersi(),
        'hitung' => commandHitung($commandArgs),
        default => unknownCommand($command),
    };
}

exit(main($argv));