0

Command registration and dispatch

Intermediate5 min read·fw-10-003

Concept

Command registration and dispatch is how the console Application knows which commands exist and how it routes php artisan <command-name> to the correct class.

Registration: Commands are added to the Application using add(Command $command). The application stores them by name: ['make:model' => MakModelCommand, 'migrate' => MigrateCommand, ...].

Dispatch: Application::run() parses argv to extract the command name (the first argument after the script name). It looks up the command in its registry and calls command->run($input, $output).

Command discovery patterns:

  1. Manual registration: Explicitly call $app->add(new MakeModelCommand()) for each command.
  2. Kernel registration: $kernel->commands([MakeModelCommand::class, MigrateCommand::class]).
  3. Auto-discovery: Scan a directory (e.g., app/Console/Commands/), find all classes extending Command, resolve from the container.
  4. Service providers: Commands registered in a service provider's boot() method.

The help command: php artisan help <command> prints the command's description, arguments, and options. Built by parsing the signature.

The list command: php artisan list shows all registered commands, grouped by namespace (the part before :). Auto-generated from the command registry.

Exit code propagation: The command returns an int from handle(). The Application::run() returns that int. The shell receives it. exit(0) = success. exit(1) = failure. CI/CD systems check this.

Code Example

php
<?php
namespace Framework\Console;

class Application
{
    private array $commands = [];

    public function __construct(
        private readonly \Framework\Container\Container $container,
        private readonly string $name    = 'Console',
        private readonly string $version = '1.0.0',
    ) {
        $this->registerBuiltInCommands();
    }

    public function add(Command $command): static
    {
        $this->commands[$command->getName()] = $command;
        return $this;
    }

    public function addCommands(array $commandClasses): static
    {
        foreach ($commandClasses as $class) {
            $this->add($this->container->make($class));
        }
        return $this;
    }

    public function run(): int
    {
        $argv        = $_SERVER['argv'] ?? [];
        $commandName = $argv[1] ?? 'list';

        if (in_array($commandName, ['-h', '--help', 'help'])) {
            $commandName = 'help';
        }

        if (!isset($this->commands[$commandName])) {
            fwrite(STDERR, "Command [{$commandName}] not found.\n");
            $this->listCommands();
            return 1;
        }

        $command = $this->commands[$commandName];
        $input   = new ArgvInput(array_slice($argv, 2), $command->getSignature());
        $output  = new ConsoleOutput();

        try {
            return $command->run($input, $output);
        } catch (\Throwable $e) {
            fwrite(STDERR, "Error: {$e->getMessage()}\n");
            return 1;
        }
    }

    private function listCommands(): void
    {
        echo "{$this->name} {$this->version}\n\nAvailable commands:\n";

        $grouped = [];
        foreach ($this->commands as $name => $command) {
            $namespace = str_contains($name, ':') ? explode(':', $name)[0] : '';
            $grouped[$namespace][$name] = $command->getDescription();
        }

        ksort($grouped);
        foreach ($grouped as $ns => $commands) {
            if ($ns) echo "\n {$ns}\n";
            foreach ($commands as $name => $desc) {
                printf("  %-30s %s\n", $name, $desc);
            }
        }
    }

    private function registerBuiltInCommands(): void
    {
        // Built-in framework commands registered here
        // $this->add(new ListCommand($this));
        // $this->add(new HelpCommand($this));
    }
}