yiisoft/yii-console
Yii Console provides a lightweight framework for building command-line applications with Yii3. Define commands, route arguments and options, handle input/output, generate help, and manage exit codes. It includes a console runner, command routing, and flexible I/O abstractions for extensible CLI tool...
composer require yiisoft/yii-console. In a Yii3 application template it is already present (usually pulled in by yiisoft/yii-runner-console), which also gives you the yii entry script at the project root../yii (or vendor/bin/yii) — the runner entrypoint.config/console.php / config/commands.php — where Application and CommandLoader are wired.vendor/yiisoft/yii-console/src/ — a small surface: Application, Command, CommandLoader, ExitCode, Stdio. Read it in ~20 minutes.vendor/symfony/console — 90% of behavior lives here; yii-console is a thin DI/loader layer on top../yii list, ./yii help, ./yii help <command>../yii hello):<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Yiisoft\Yii\Console\Command;
use Yiisoft\Yii\Console\ExitCode;
final class HelloCommand extends Command
{
public function __construct(private readonly Clock $clock)
{
parent::__construct();
}
protected function configure(): void
{
$this->setName('hello')
->setDescription('Say hello');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Hello, ' . $this->clock->now()->format('H:i'));
return ExitCode::OK;
}
}
cache:clear, db:seed, user:create — reusing the services you already register in the container instead of bootstrapping by hand.Pick a base class
Yiisoft\Yii\Console\Command for app commands — DI-friendly convenience base.Symfony\Component\Console\Command\Command directly when reusing third-party commands.#[ConsoleCommand(name: 'user:create', description: '...', hidden: false)] on the class when your loader supports attribute discovery; keep argument/option declarations in configure() regardless.Constructor injection is the point
CommandLoader is lazy: heavy dependencies are only built when the command actually runs, so ./yii list stays fast.execute().Arguments and options
InputArgument::REQUIRED|OPTIONAL|IS_ARRAY and InputOption::VALUE_NONE|VALUE_REQUIRED|VALUE_OPTIONAL|VALUE_IS_ARRAY|VALUE_NEGATABLE.configure(), read with $input->getArgument() / $input->getOption().--dry-run / --force / --limit conventions.Output and UX
SymfonyStyle for tables, sections, progress bars and questions: $io = new SymfonyStyle($input, $output); $io->table(...).$output->writeln($msg, OutputInterface::VERBOSITY_VERBOSE).$output->getErrorOutput() or fwrite(STDERR, ...)), never mix them into machine-readable stdout.$input->isInteractive(); always support -n / --no-interaction.Exit codes
ExitCode::OK (0) for success; non-zero for failure.RuntimeException is fine — the Application renders it — but returning 0 from a failed command is the most common CI bug.Startup / shutdown hooks
ApplicationStartup and ApplicationShutdown for timing, logging, advisory locks, memory guards and cleanup.Application::run(), not for commands invoked programmatically — don't rely on them for invariants inside the command itself.Long-running commands
SignalableCommandInterface and handle SIGINT/SIGTERM for a clean exit code.--limit / --max-iterations, print progress, and make commands idempotent.Testing
CommandTester / ApplicationTester from symfony/console; build the command with real services or test doubles.getStatusCode() and getDisplay(); use setInputs() for prompts and ['interactive' => false] in CI.Registration and naming
config/commands.php, or the CommandLoader commands map, or attribute scan).: (user:create, cache:clear) so ./yii list groups them.execute() is not a command until the loader/container knows it. Attribute discovery only works when the loader is configured to scan; otherwise add it to the commands map. ./yii list is the first debug step.exit() inside execute() breaks everything — no shutdown events, no testability, CommandTester can't capture the code. Return the int.help, list, completion, --help, --version, --quiet, --verbose, --no-interaction are Symfony-owned. Don't shadow them.setAutoExit(false) and setCatchExceptions(false) in tests so failures surface instead of being swallowed and rendered by the Application.list. DB connects, HTTP clients and similar belong behind lazy container resolution inside execute(). If ./yii list takes more than ~200ms, you're bootstrapping too much.OutputInterface vs ConsoleOutputInterface. Check/cast before calling getErrorOutput(). Under -q (common in CI) all writeln output disappears — write errors to STDERR so they survive.$input->isInteractive(), and guard fgets(STDIN)/QuestionHelper too — STDIN may not exist without a tty.null from execute() is fatal in Symfony Console. Return an int or throw../yii list -vvv, ./yii help <cmd>, and fwrite(STDERR, ...) for ad-hoc dumps so machine-readable stdout stays clean.Application is a Symfony Application — add(), getDefinition(), getHelperSet(), setCatchExceptions() all work. For middleware-like behavior, wrap the runner in the yii script rather than monkey-patching commands../yii <command> into cron/systemd timers; keep commands idempotent and safe to re-run.yiisoft/yii-console tracks Symfony Console majors; a Symfony bump can change helpers and SignalableCommandInterface behavior. Read the changelog before a major upgrade and run your command test suite.How can I help you explore Laravel packages today?