Creating Console Commands
A command is a class extending
Symfony\Component\Console\Command\Command.
Dependencies are passed through the constructor (container autowiring);
logic lives in execute().
See also:
Console
(providers and dependencies,
DisabledCommand),
autowiring.
Symfony documentation: Creating a Command, Input, Output styling.
Minimal command
<?php declare(strict_types=1);
namespace Concept\App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
final class GreetCommand extends Command
{
protected function configure(): void
{
$this->setName('app:greet')
->setDescription('Print a greeting');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->success('Hello from Concept Core');
return Command::SUCCESS;
}
}
Application registration
Add the class to config/commands.php:
return [
'commands' => [
// ...core commands
GreetCommand::class,
],
];
ConsoleServiceProvider calls $container->get(GreetCommand::class)
and addCommand(). If the constructor needs services — they are resolved automatically
through ReflectionContainer.
If a dependency is not registered (required provider missing from
bootstrap/providers/console.php), your command is replaced by a
DisabledCommand — see Console → disabled.
php bin/console.php app:greet
Providers for your command
Typical custom command dependencies and what to enable in CLI:
| Constructor dependency | Provider in console.php |
|---|---|
UserModel, DatabaseInterface | DatabaseServiceProvider |
ConfigInterface | ConfigServiceProvider |
RouterInterface | HttpServiceProvider |
LoggerInterface | LogServiceProvider |
| component service | component provider + ComponentsServiceProvider |
A command without a constructor (like GreetCommand) depends only on
ConsoleServiceProvider. After changing console.php, rerun
php bin/console.php list and confirm there is no red “disabled” warning.
Arguments and options
Example — UserListCommand (user:list):
use Concept\Components\AuthAdmin\Models\UserModel;
use Symfony\Component\Console\Input\InputOption;
class UserListCommand extends Command
{
private const string COMMAND_NAME = 'user:list';
protected function configure(): void
{
$this->setName(self::COMMAND_NAME)
->setDescription('Display a list of registered users')
->addOption('limit', 'l', InputOption::VALUE_OPTIONAL, 'How many users?', 10);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$limit = is_numeric($input->getOption('limit'))
? (int) $input->getOption('limit')
: 10;
$users = UserModel::query()->limit($limit)->get()->toArray();
$io->table(array_keys($users[0] ?? []), $users);
return Command::SUCCESS;
}
}
For consistency with models via DI, prefer injecting
UserModel and calling $this->userModel->newQuery():
public function __construct(
private readonly UserModel $userModel,
) {
parent::__construct();
}
DatabaseServiceProvider is required in CLI — otherwise the command becomes disabled.
Core example: DI in a command
RouteListCommand receives RouterInterface — requires HttpServiceProvider:
use Concept\Core\Http\Routing\Contracts\RouterInterface;
public function __construct(private readonly RouterInterface $router)
{
parent::__construct();
}
DbMigrateCommand — Migrator + MigrationRegistry.
Complex core commands live in Concept\Core\Console\Commands.
Command in a component
- Create
Components/MyModule/Commands/MyCommand.php - Add the class to
commands()in the component class - Ensure the component is enabled in
config/components.php
// AuthAdminComponent::commands()
public function commands(): array
{
return [
UserListCommand::class,
];
}
During ComponentsServiceProvider::boot() commands are resolved from the container and
added to ConsoleApplication via addCommand().
ConsoleServiceProvider must be registered earlier in
bootstrap/providers/console.php.
If a component command needs DB or HTTP services — the corresponding core providers are still required in CLI (see table above).
Output and errors
$io = new SymfonyStyle($input, $output);
$io->title('Import started');
$io->writeln('Processing...');
$io->success('Done');
$io->warning('Partial import');
$io->error('Failed');
// table
$io->table(['Column A', 'Column B'], $rows);
// progress (long loops)
$io->progressStart(count($items));
foreach ($items as $item) {
// ...
$io->progressAdvance();
}
$io->progressFinish();
On exception return Command::FAILURE or let Symfony print the stack trace
(in dev with Whoops for CLI — plain text).
Checklist
- Unique command name (
module:action, e.g.billing:sync) configure()— name, description, arguments/options;COMMAND_NAMEconstant like in coreexecute()— short orchestration, heavy work in a service;Command::SUCCESS/FAILURE- Registration in
config/commands.phporcommands()on the component class - CLI providers cover all constructor dependencies
- Verify:
php bin/console.php list(no “disabled”) andhelp your:command
Do not duplicate HTTP controllers in CLI: move shared logic to Services/
and call it from both web and command.
A “disabled” command is almost always a missing provider in console.php, not a bug in execute().