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 dependencyProvider in console.php
UserModel, DatabaseInterfaceDatabaseServiceProvider
ConfigInterfaceConfigServiceProvider
RouterInterfaceHttpServiceProvider
LoggerInterfaceLogServiceProvider
component servicecomponent 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();
}

DbMigrateCommandMigrator + MigrationRegistry. Complex core commands live in Concept\Core\Console\Commands.

Command in a component

  1. Create Components/MyModule/Commands/MyCommand.php
  2. Add the class to commands() in the component class
  3. 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

  1. Unique command name (module:action, e.g. billing:sync)
  2. configure() — name, description, arguments/options; COMMAND_NAME constant like in core
  3. execute() — short orchestration, heavy work in a service; Command::SUCCESS / FAILURE
  4. Registration in config/commands.php or commands() on the component class
  5. CLI providers cover all constructor dependencies
  6. Verify: php bin/console.php list (no “disabled”) and help 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().