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
// 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:
2 + 3 = 5Mari kita telaah baris-baris pentingnya:
- Tabel
$commands: satu-satunya tempat pendaftaran perintah. Menambah kemampuan baru berarti menambah satu entri dan satu handler — tanpa menyentuh logika dispatch. $perintah = $argv[1] ?? "help": argumen pertama adalah nama perintah; bila kosong, aplikasi jatuh kehelp.array_slice($argv, 2): handler hanya menerima argumen milik perintahnya, bukan nama perintah itu sendiri.- Guard perintah tak dikenal: pesan dikirim ke
STDERRlaluexit(1)— pengguna salah mengetik bukan berarti programnya gagal. exit($commands[$perintah]($args)): nilai balik handler (0sukses,2salah 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
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:
+----------------+---------------------------------+
| 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:
php app.phpOutput yang Dihasilkan:
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
// 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:
Di terminal sungguhan, kedua jalur kegagalan terlihat lengkap beserta kode keluarnya:
php app.php hitung 7
echo "exit code: $?"Output yang Dihasilkan:
Galat: pemakaian hitung <a> <b>
exit code: 2php app.php foobar
echo "exit code: $?"Output yang Dihasilkan:
Galat: perintah tidak dikenal: foobar
exit code: 1Warna 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
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:
composer require symfony/consoleSetiap 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
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#!/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));