Updated June 2026. Tested on Symfony 7.x and PHP 8.4. Part of the Techalyst Symfony series.
Not everything an app does happens in response to a web request. Cron jobs, data imports, nightly cleanups, one-off maintenance, these run on the command line. Symfony's Console component lets you write these tasks as commands that have full access to your services, so a scheduled job can use the same repositories and mailers as your controllers. If you have run php bin/console for anything, you have already used it.
A command class
A command is a class marked with the #[AsCommand] attribute and a name. The work goes in execute, and you inject whatever services you need through the constructor, the same autowiring as everywhere else:
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(name: 'app:send-reminders', description: 'Send any due reminders')]
class SendRemindersCommand extends Command
{
public function __construct(private ReminderService $reminders)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$count = $this->reminders->sendDue();
$io->success("Sent {$count} reminders.");
return Command::SUCCESS;
}
}
Run it with php bin/console app:send-reminders, and it appears in the command list with its description. make:command scaffolds this skeleton for you.
Arguments and options
Commands take input. Arguments are positional, options are flags. You declare them in a configure method and read them from the input:
protected function configure(): void
{
$this->addArgument('email', InputArgument::REQUIRED, 'Who to remind')
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do not actually send');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$email = $input->getArgument('email');
$dryRun = $input->getOption('dry-run');
// ...
}
Now php bin/console app:send-reminders jane@example.com --dry-run passes both in.
Nice output with SymfonyStyle
SymfonyStyle, the $io above, gives you readable, consistent output without fiddling with formatting. It covers the things CLI tasks actually need:
$io->title('Importing products');
$io->progressBar(count($rows)); // a progress bar for long jobs
$io->table(['Name', 'Price'], $rows);
$io->error('Something went wrong.');
$name = $io->ask('Product name?');
It makes a command feel polished with almost no effort.
Return a status code
A command must return an integer exit code: Command::SUCCESS (0) when it worked, or Command::FAILURE (1) when it did not. This matters because cron and CI check that code to know whether the job succeeded, so returning the right one is how the outside world learns your import failed.
Scheduling
A console command is just a CLI program, so the simplest way to run it on a schedule is a cron entry calling php bin/console app:send-reminders. For schedules managed inside the app, Symfony also has a Scheduler component, but a cron line is all most tasks need.
Wrapping up
Console commands are how you do work outside the web request: a class with #[AsCommand], the logic in execute, and services injected through the constructor. Declare arguments and options in configure and read them from the input, use SymfonyStyle for clean output and progress bars, and return Command::SUCCESS or Command::FAILURE so schedulers know the outcome. Run them by hand with bin/console or on a cron schedule, and your background tasks share all the same services as the rest of the app.
All comments ()
No comments yet
Be the first to leave a comment on this post.