Skrip satu berkas memang cukup untuk tugas kecil, tetapi begitu sebuah tool memiliki empat atau lima perilaku berbeda, memanggilnya lewat flag yang saling bertabrakan cepat menjadi membingungkan. Aplikasi console mengatasi masalah ini dengan pola yang dipakai hampir semua tool Unix: satu berkas masuk, banyak perintah di dalamnya — php app.php <perintah> [argumen...]. Perintah git, composer, dan artisan bekerja dengan cara yang sama. Pada chapter ini kita akan membangun kerangka aplikasi console mini dari nol: tabel dispatch perintah, output bantuan berbentuk tabel, pemetaan exit code per perintah, hingga pengantar Symfony Console untuk skala produksi.

Pola Dispatch Perintah #

Inti aplikasi console adalah tabel asosiatif yang memetakan nama perintah ke handler — fungsi yang menerima sisa argumen, mencetak output, dan mengembalikan exit code sebagai integer. Baris terakhir program tinggal meneruskan nilai balik handler itu ke exit(), sehingga setiap perintah bebas menentukan kode keluarnya sendiri. Karena verifikasi otomatis chapter ini menjalankan kode tanpa argumen tambahan, contoh berikut mengisi $argv secara eksplisit; di aplikasi nyata baris simulasi itu dihapus dan PHP mengisi $argv langsung dari baris perintah.

php
<?php
// Simulasi CLI: php app.php hitung 2 3
$argv = ["app.php", "hitung", "2", "3"];

$commands = [
    "sapa" => "cmdSapa",
    "hitung" => "cmdHitung",
];

function cmdSapa(array $args): int
{
    $nama = $args[0] ?? "Dunia";
    echo "Halo, $nama!\n";
    return 0;
}

function cmdHitung(array $args): int
{
    if (count($args) < 2) {
        fwrite(STDERR, "Pemakaian: hitung <a> <b>\n");
        return 2;
    }
    printf("%d + %d = %d\n", (int) $args[0], (int) $args[1], (int) $args[0] + (int) $args[1]);
    return 0;
}

$perintah = $argv[1] ?? "help";
$args = array_slice($argv, 2);

if (!isset($commands[$perintah])) {
    fwrite(STDERR, "Perintah tidak dikenal: $perintah\n");
    exit(1);
}

exit($commands[$perintah]($args));

Output yang Dihasilkan:

text
2 + 3 = 5

Mari kita telaah baris-baris pentingnya:

  1. Tabel $commands: satu-satunya tempat pendaftaran perintah. Menambah kemampuan baru berarti menambah satu entri dan satu handler — tanpa menyentuh logika dispatch.
  2. $perintah = $argv[1] ?? "help": argumen pertama adalah nama perintah; bila kosong, aplikasi jatuh ke help.
  3. array_slice($argv, 2): handler hanya menerima argumen milik perintahnya, bukan nama perintah itu sendiri.
  4. Guard perintah tak dikenal: pesan dikirim ke STDERR lalu exit(1) — pengguna salah mengetik bukan berarti programnya gagal.
  5. exit($commands[$perintah]($args)): nilai balik handler (0 sukses, 2 salah pemakaian) menjadi exit code seluruh aplikasi.

Alur lengkapnya dapat diringkas dalam diagram berikut — setiap eksekusi berjalan dari argumen ke tabel, lalu berakhir pada satu angka exit code:

Berkas pendamping app.php pada bagian bawah halaman memuat versi lengkap kerangka ini dengan tiga perintah siap pakai, termasuk tabel help dan deteksi terminal.

Output Bantuan dan Tabel Terstruktur #

Perintah help adalah kontrak antara tool dan penggunanya, jadi tampilannya perlu rapi dan selalu sinkron dengan tabel dispatch. Cara paling sederhana menjaga sinkronisasi: merender tabel langsung dari data yang sama. Fungsi renderTable() berikut menghitung lebar tiap kolom dengan mb_strlen() (aman untuk teks UTF-8), meratakan sel dengan str_pad(), lalu membungkusnya dengan garis pemisah:

php
<?php
function renderTable(array $headers, array $rows): void
{
    $widths = [];
    foreach ($headers as $i => $header) {
        $widths[$i] = mb_strlen($header);
    }
    foreach ($rows as $row) {
        foreach (array_values($row) as $i => $cell) {
            $widths[$i] = max($widths[$i], mb_strlen((string) $cell));
        }
    }

    $renderRow = function (array $cells) use ($widths): string {
        $line = "| ";
        foreach (array_values($cells) as $i => $cell) {
            $line .= str_pad((string) $cell, $widths[$i]) . " | ";
        }
        return rtrim($line);
    };

    $separator = "+";
    foreach ($widths as $width) {
        $separator .= str_repeat("-", $width + 2) . "+";
    }

    echo $separator, "\n";
    echo $renderRow($headers), "\n";
    echo $separator, "\n";
    foreach ($rows as $row) {
        echo $renderRow($row), "\n";
    }
    echo $separator, "\n";
}

renderTable(
    ["Perintah", "Deskripsi"],
    [
        ["help", "Menampilkan daftar perintah"],
        ["sapa <nama>", "Menyapa seseorang"],
        ["hitung <a> <b>", "Menjumlahkan dua bilangan bulat"],
    ]
);

Output yang Dihasilkan:

text
+----------------+---------------------------------+
| Perintah       | Deskripsi                       |
+----------------+---------------------------------+
| help           | Menampilkan daftar perintah     |
| sapa <nama>    | Menyapa seseorang               |
| hitung <a> <b> | Menjumlahkan dua bilangan bulat |
+----------------+---------------------------------+

Lebar kolom diambil dari sel terpanjang, sehingga menambah baris baru tidak perlu menyentuh kode tampilan. Inilah tampilan app.php yang sama saat dipanggil tanpa argumen — aplikasi memilih help sebagai perintah bawaan:

bash
php app.php

Output yang Dihasilkan:

text
salam 1.0.0 - aplikasi console mini PHP

Pemakaian:
  php app.php <perintah> [argumen...]

Perintah tersedia:
+----------------+---------------------------------+
| Perintah       | Deskripsi                       |
+----------------+---------------------------------+
| help           | Menampilkan daftar perintah     |
| sapa <nama>    | Menyapa seseorang               |
| hitung <a> <b> | Menjumlahkan dua bilangan bulat |
+----------------+---------------------------------+

Exit Code Setiap Perintah #

Pemetaan exit code pada aplikasi console mengikuti konvensi yang sudah kita bahas pada chapter Exit Codes : 0 untuk sukses, 2 untuk salah pemakaian, dan 1 untuk kegagalan umum seperti perintah yang tidak dikenal. Karena setiap handler mengembalikan integer, kontrak ini dijaga di satu tempat — ujung program. Contoh berikut menunjukkan jalur hitung ketika argumennya kurang: handler menulis pesan ke STDERR dan mengembalikan 2, sementara stdout tetap kosong.

php
<?php
// Simulasi CLI: php app.php hitung 7 (argumen kurang)
$args = ["7"];

function cmdHitung(array $args): int
{
    if (count($args) < 2) {
        fwrite(STDERR, "Pemakaian: hitung <a> <b>\n");
        return 2;
    }
    printf("%d + %d = %d\n", (int) $args[0], (int) $args[1], (int) $args[0] + (int) $args[1]);
    return 0;
}

exit(cmdHitung($args));

Output yang Dihasilkan:

text

Di terminal sungguhan, kedua jalur kegagalan terlihat lengkap beserta kode keluarnya:

bash
php app.php hitung 7
echo "exit code: $?"

Output yang Dihasilkan:

text
Galat: pemakaian hitung <a> <b>
exit code: 2
bash
php app.php foobar
echo "exit code: $?"

Output yang Dihasilkan:

text
Galat: perintah tidak dikenal: foobar
exit code: 1

Warna Saat Output Menuju Terminal #

Tool yang ramah memakai warna di terminal, tetapi kode ANSI yang sama menjadi sampah saat output diarahkan ke berkas log. Solusinya: periksa dulu apakah STDOUT benar-benar terminal dengan posix_isatty() — persis seperti yang dipakai app.php:

php
<?php
function colorize(string $text, string $color): string
{
    if (!posix_isatty(STDOUT)) {
        return $text; // output sedang di-pipe atau dialihkan: polos saja
    }

    $codes = ["hijau" => "32", "merah" => "31", "kuning" => "33"];
    return "\033[" . $codes[$color] . "m" . $text . "\033[0m";
}

Saat Anda menjalankan php app.php sapa Budi langsung di terminal, sapaan tampil hijau; saat output yang sama dipipe ke grep atau tee, teks kembali polos tanpa karakter kendali. Pola satu fungsi ini menjaga log tetap bersih sekaligus tampilan tetap hidup.

Symfony Console untuk Tool yang Lebih Besar #

Kerangka mini di atas bagus untuk belajar dan untuk tool internal kecil. Begitu tool Anda menangani banyak opsi, validasi argumen, interaksi prompt, atau sub-perintah bertingkat, gunakan paket symfony/console yang sudah menguji semua detail itu. Pemasangannya lewat Composer:

bash
composer require symfony/console

Setiap perintah menjadi class tersendiri dengan deklarasi argumen dan nilai balik exit code yang eksplisit — Command::SUCCESS (0), Command::FAILURE (1), dan Command::INVALID (2), selaras dengan konvensi yang kita pakai sejauh ini:

php
<?php

declare(strict_types=1);

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(name: "app:sapa", description: "Menyapa seseorang")]
class SapaCommand extends Command
{
    protected function configure(): void
    {
        $this->addArgument("nama", InputArgument::OPTIONAL, "Nama yang disapa", "Dunia");
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $nama = $input->getArgument("nama");
        $output->writeln("Halo, $nama!");

        return Command::SUCCESS;
    }
}

Dengan pendekatan ini, Symfony Console menghadirkan --help dan --version otomatis untuk setiap perintah, merender tabel siap pakai, serta menyediakan helper pertanyaan interaktif. Arsitektur dispatch tetap sama dengan kerangka mini kita — tabel perintah, handler, exit code — hanya kali ini tabelnya dikelola paket yang teruji di jutaan proyek.

Ringkasan #

Aplikasi console yang rapi terdiri dari tabel dispatch, handler yang mengembalikan exit code, output bantuan yang dirender dari data yang sama dengan dispatch, serta deteksi terminal untuk warna. Pola php app.php <perintah> [argumen...] ini menjadikan tool Anda mudah dikombinasikan dengan shell dan pipeline. Setelah tool dapat dijalankan dengan andal, langkah berikutnya adalah menjalankannya tanpa manusia — lewat penjadwalan cron, yang kita bahas pada chapter Scheduled Jobs.

Referensi #

Berkas Sumber

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

app.php

Unduh berkas
php
#!/usr/bin/env php
<?php

declare(strict_types=1);

// app.php — aplikasi console mini dalam satu berkas.
// Jalankan: php app.php <perintah> [argumen...]

function colorize(string $text, string $color): string
{
    if (!posix_isatty(STDOUT)) {
        return $text;
    }

    $codes = ["hijau" => "32", "merah" => "31", "kuning" => "33"];
    return "\033[" . $codes[$color] . "m" . $text . "\033[0m";
}

function renderTable(array $headers, array $rows): void
{
    $widths = [];
    foreach ($headers as $i => $header) {
        $widths[$i] = mb_strlen($header);
    }
    foreach ($rows as $row) {
        foreach (array_values($row) as $i => $cell) {
            $widths[$i] = max($widths[$i], mb_strlen((string) $cell));
        }
    }

    $renderRow = function (array $cells) use ($widths): string {
        $line = "| ";
        foreach (array_values($cells) as $i => $cell) {
            $line .= str_pad((string) $cell, $widths[$i]) . " | ";
        }
        return rtrim($line);
    };

    $separator = "+";
    foreach ($widths as $width) {
        $separator .= str_repeat("-", $width + 2) . "+";
    }

    echo $separator, "\n";
    echo $renderRow($headers), "\n";
    echo $separator, "\n";
    foreach ($rows as $row) {
        echo $renderRow($row), "\n";
    }
    echo $separator, "\n";
}

function cmdHelp(array $args): int
{
    echo "salam 1.0.0 - aplikasi console mini PHP\n\n";
    echo "Pemakaian:\n";
    echo "  php app.php <perintah> [argumen...]\n\n";
    echo "Perintah tersedia:\n";
    renderTable(
        ["Perintah", "Deskripsi"],
        [
            ["help", "Menampilkan daftar perintah"],
            ["sapa <nama>", "Menyapa seseorang"],
            ["hitung <a> <b>", "Menjumlahkan dua bilangan bulat"],
        ]
    );

    return 0;
}

function cmdSapa(array $args): int
{
    $nama = $args[0] ?? "Dunia";
    echo colorize("Halo, $nama!", "hijau") . "\n";

    return 0;
}

function cmdHitung(array $args): int
{
    if (count($args) < 2) {
        fwrite(STDERR, colorize("Galat", "merah") . ": pemakaian hitung <a> <b>\n");
        return 2;
    }

    printf("%d + %d = %d\n", (int) $args[0], (int) $args[1], (int) $args[0] + (int) $args[1]);

    return 0;
}

$command = $argv[1] ?? "help";
$args = array_slice($argv, 2);

$commands = [
    "help" => "cmdHelp",
    "sapa" => "cmdSapa",
    "hitung" => "cmdHitung",
];

if (!isset($commands[$command])) {
    fwrite(STDERR, colorize("Galat", "merah") . ": perintah tidak dikenal: $command\n");
    exit(1);
}

exit($commands[$command]($args));