Skip to Content
Course content

239: Building CLI Tools with Symfony Console

Click on the "Edit" button in the top corner of the screen to edit your slide content.

How do I actually structure a command so Symfony knows what to do?

You could technically write a raw PHP script and run it with php script.php, but that gets messy fast once you need help menus, validation, or complex arguments. With Symfony Console, everything revolves around a class that extends Command. I always tell people to think of it in two phases: the configuration (what the command is called and what it expects) and the execution (the actual logic).

Let's say we're building a tool to sync products from a CSV file into our database. Here is the skeletal structure you'll use:

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class SyncProductsCommand extends Command
{
    protected static $defaultName = 'app:sync-products';

    protected function configure(): void
    {
        $this
            ->setDescription('Imports products from a CSV file.')
            ->setHelp('This command allows you to bulk upload products to the catalog.');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // Your logic goes here
        $output->writeln('Starting the sync process...');
        
        return Command::SUCCESS;
    }
}

Notice the return Command::SUCCESS;. Symfony expects an integer return value. If something goes south, return Command::FAILURE. This is huge for CI/CD pipelines because it tells the server whether the deployment script should stop or keep going.

What's the difference between an argument and an option, and how do I add them?

This is where a lot of people trip up. In my experience, the simplest way to remember it is: Arguments are the "what" (required or optional values), and Options are the "how" (flags that modify behavior).

For our product sync, we definitely need the path to the CSV file (Argument). But we might also want a --dry-run flag so we can see what would happen without actually touching the database (Option). You define these in the configure() method:

use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

protected function configure(): void
{
    $this
        ->addArgument('filename', InputArgument::REQUIRED, 'The path to the CSV file')
        ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Check the file without importing');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
    $file = $input->getArgument('filename');
    $isDryRun = $input->getOption('dry-run');

    if ($isDryRun) {
        $output->writeln("Dry run enabled. No data will be saved for $file.");
    }

    return Command::SUCCESS;
}

Using InputOption::VALUE_NONE means the user doesn't have to pass a value (like --dry-run=yes); just presence of the flag makes it true.

How do I stop the output from looking like a wall of plain text?

If you just use $output->writeln(), your tool looks like it was written in 1992. Symfony provides a wrapper called SymfonyStyle that makes your CLI tool look like a professional product. It gives you pre-styled blocks for success, errors, warnings, and even progress bars.

I almost always instantiate SymfonyStyle at the very top of my execute method. Here is how you'd use it to make a product import feel responsive:

use Symfony\Component\Console\Style\SymfonyStyle;

protected function execute(InputInterface $input, OutputInterface $output): int
{
    $io = new SymfonyStyle($input, $output);
    $io->title('Product Catalog Importer');

    $file = $input->getArgument('filename');
    
    if (!file_exists($file)) {
        $io->error("The file $file was not found!");
        return Command::FAILURE;
    }

    $io->section('Processing items...');
    $progressBar = $io->createProgressBar(100); 
    $progressBar->start();

    // Simulate processing
    for ($i = 0; $i < 100; $i++) {
        usleep(10000);
        $progressBar->advance();
    }

    $progressBar->finish();
    $io->newLine(2);
    $io->success('All products have been synced successfully!');

    return Command::SUCCESS;
}

The $io->success() and $io->error() methods add background colors and icons automatically. It's a small touch, but it makes a massive difference in how other developers perceive your tools.




📋 Practical Task

Exercise: Build a "Database User Deactivator" Tool

Your task is to create a Symfony Console command called app:user:deactivate. This tool will be used by admins to disable user accounts in the system.

Requirements:

  • Argument: The command must require a userId (integer).
  • Option: Add a --reason option that allows the admin to provide a reason for deactivation (this should be optional and accept a string value).
  • UX: Use SymfonyStyle to:
    • Display a confirmation question (confirm()) asking "Are you sure you want to deactivate user [ID]?" before proceeding.
    • If the user says "no", display a warning saying "Operation cancelled." and exit.
    • If the user says "yes", display a success message stating the user has been deactivated, including the reason if one was provided.

Note: You don't need to connect to a real database; just simulate the logic by printing the results to the console.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.