Belajar Symfony - Console & CLI Commands
Episode 9 of 27

Belajar Symfony - Console & CLI Commands

Membangun CLI dengan Symfony Console: membuat command sendiri memakai attributes, arguments & options, input/output interaktif (table, progress bar), exit codes, serta praktik nyata CLI tooling seperti import data dan task terjadwal lewat cron.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Sejauh ini semua berjalan lewat HTTP. Episode 9 menyeberang ke dunia yang tak kalah penting di produksi: command line. Setiap aplikasi nyata butuh pekerjaan yang tidak dipicu browser — import data, kirim reminder, generate laporan, clean-up file. Symfony menyediakan komponen Console untuk itu, dan bin/console yang kalian pakai sejak episode 3 sebenarnya adalah wujudnya.

Mengapa command penting? Karena inilah fondasi otomasi: task yang bisa dijadwalkan dengan cron, dipicu dari pipeline CI/CD, atau dijalankan manual oleh operator — dengan output yang bisa dibaca mesin (exit code) dan manusia (progress bar).

Anatomi Command

Buat command pertama:

Buat command ImportPosts
php bin/console make:command app:import-posts

Hasilnya adalah class yang mewarisi Command dengan attribute #[AsCommand]:

src/Command/ImportPostsCommand.php
<?php
 
namespace App\Command;
 
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
 
#[AsCommand(
    name: 'app:import-posts',
    description: 'Mengimpor artikel dari file JSON.',
)]
class ImportPostsCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // logika import...
        $output->writeln('Import selesai.');
 
        return Command::SUCCESS;
    }
}

Aturan emas: execute() harus mengembalikan exit codeCommand::SUCCESS (0), Command::FAILURE (1), atau Command::INVALID (2). Inilah yang membuat command bisa dipakai di script dan CI dengan andal.

Arguments dan Options

Bedakan keduanya:

  • Argument — nilai posisional, wajib atau opsional: app:import-posts file.json.
  • Option — flag bernama, opsional: app:import-posts --dry-run.
Arguments dan options
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
 
protected function configure(): void
{
    $this
        ->addArgument('file', InputArgument::REQUIRED, 'Path file JSON')
        ->addArgument('limit', InputArgument::OPTIONAL, 'Batasi jumlah post', 100)
        ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Tampilkan tanpa menulis');
}

Membacanya di execute():

Membaca input
$file = $input->getArgument('file');
$limit = (int) $input->getArgument('limit');
$isDryRun = $input->getOption('dry-run');
 
if ($isDryRun) {
    $output->writeln("DRY-RUN: {$file} (maks {$limit} post)");
    return Command::SUCCESS;
}

VALUE_NONE berarti option bertipe boolean (--dry-run ada = true). Untuk nilai yang butuh parameter, pakai VALUE_REQUIRED.

Output yang Enak Dibaca: Table dan Progress Bar

Output polos cukup, tapi untuk UX CLI yang profesional gunakan SymfonyStyle:

SymfonyStyle — table & progress
use Symfony\Component\Console\Style\SymfonyStyle;
 
protected function execute(InputInterface $input, OutputInterface $output): int
{
    $io = new SymfonyStyle($input, $output);
 
    $rows = [
        ['123', 'Artikel demo', 'published'],
        ['124', 'Artikel draft', 'draft'],
    ];
    $io->table(['ID', 'Judul', 'Status'], $rows);
 
    $io->section('Memproses...');
    $io->progressStart(count($rows));
    foreach ($rows as $row) {
        // kerja per baris...
        usleep(100000);
        $io->progressAdvance();
    }
    $io->progressFinish();
 
    $io->success('Selesai.');
    $io->warning('Beberapa post dilewati.');
 
    return Command::SUCCESS;
}
HelperFungsi
$io->table()Render tabel dengan header
$io->progressStart/Advance/FinishProgress bar
$io->success/warning/errorPesan berwarna berikon
$io->ask()/confirm()/choice()Input interaktif

Output di bawah akan tampak seperti ini di terminal:

Contoh output
 ---------- ------------- -------- 
  ID         Judul         Status  
 ---------- ------------- -------- 
  123        Artikel demo  published
  124        Artikel draft draft   
 ---------- ------------- -------- 
 
  Memproses...
 8/8 [============================] 100%
 
 // Success: Selesai.

Layanan Service di dalam Command

Command adalah service — jadi kalian bisa menyuntikkan service apa pun via constructor:

Command memakai service
public function __construct(
    private readonly ArticleRepository $articleRepository,
    private readonly LoggerInterface $logger,
) {
    parent::__construct();
}

Inilah alasan command adalah "aplikasi tanpa HTTP": logika bisnis yang sama dipakai dari web (controller) dan CLI (command) dengan cara yang konsisten — misalnya app:import-posts memakai EntityManagerInterface untuk menulis ke database.

Cron dan Tooling Nyata

Command berpasangan sempurna dengan cron:

Contoh crontab
0 2 * * * cd /srv/myapp && php bin/console app:generate-reports
30 3 * * * cd /srv/myapp && php bin/console app:cleanup --days=14

Pola umum tooling:

CommandKasus nyata
app:import-posts file.jsonImport data dari export sistem lain
app:send-remindersKirim email reminder harian
app:generate-reportsLaporan batch untuk stakeholder
app:cleanup --days=NBersihkan file/log tua

Tip

Untuk pekerjaan berat, jangan paksa command berjalan sinkron di cron — arahkan ke Messenger (episode 10) agar tugas masuk antrian dan diproses worker. Cron cukup jadi trigger, bukan eksekutor beban berat.

Common Pitfalls

  • Lupa mengembalikan exit code — PHP default mengembalikan null = sukses secara keliru; selalu return Command::SUCCESS/FAILURE.
  • Kesalahan arah arguments — posisi getArgument() tidak tergantung definisi; pastikan order saat memanggil.
  • Command private __construct — jika menambah dependency, jangan lupa memanggil parent::__construct() atau command error.

Penutup

Pada episode 9 ini, kalian telah membangun tooling CLI di atas Console.

Inti yang harus dibawa pulang:

  • Command adalah class dengan #[AsCommand] dan method execute().
  • Selalu kembalikan exit code: SUCCESS, FAILURE, atau INVALID.
  • Argument posisional vs option bernama; --dry-run untuk simulasi.
  • SymfonyStyle menghadirkan table, progress bar, dan feedback yang manusiawi.
  • Command memakai service seperti controller — logika bisnis reusable lintas konteks.

Di episode 10 selanjutnya kita melempar pekerjaan berat ke latar belakang: Messenger: Queue & Async Processing — message bus, handler, transport async (Doctrine/Redis/AMQP), retry & failure handling, dan praktik async notification dengan worker. Sampai jumpa di episode 10!