Repository navigation
Description
Summary
Replace the Symfony Application and the Symfony Command base class of CliCommand with native Quantum classes, so the console app lines up with the web app: Quantum owns the run, the exit code and the error path, and the stages (ConsoleAppAdapter) are in full control of the lifecycle.
Symfony's input, output and helper classes (ArgvInput, ArrayInput, InputDefinition, OutputInterface, Table, ProgressBar, QuestionHelper, and so on) stay, by design. symfony/console remains a dependency as the input, output and helper layer.
Milestone: 3.3.0. This is a breaking change for apps that extend Symfony Command methods in their own commands.
Why
The console adapter already runs on the same pipeline model as the web adapter (src/App/Adapters/Console/ConsoleAppAdapter.php: boot stages, one runtime stage, ConsoleContext with stop() and an exit code). What is not under Quantum's control is the run itself. src/App/Adapters/Console/Stages/Runtime/RunCommandStage.php hands everything to Symfony\Component\Console\Application::run(), which:
- parses the input and finds the command;
- runs Symfony's built-in
listandhelpcommands; - catches and prints a command's exception itself (
setCatchExceptionsis on by default), so the QuantumErrorHandleris bypassed for command errors;tests/Helpers/NonExitingApplication.phpexists only to switch this off in tests; - calls
exit(), soconsole.command.aftercan never fire in a real run (noted inCLAUDE.mdand guarded inPipelineTest).
Consequences today:
ResolveEnvironmentStagehas to special-case Symfony'slistandhelpbecause they are notCliCommands and cannot say they need no environment.- The command layer cannot be changed without changing Symfony's classes:
CliCommandis a 244-line adapter that maps Quantum's$argsand$optionsarrays onto Symfony'sInputArgumentandInputOption. - Tests of commands need Symfony's
CommandTester, which only accepts a SymfonyCommand.
Laravel does the same split and keeps Symfony underneath: Illuminate\Console\Application extends SymfonyApplication with setAutoExit(false) and setCatchExceptions(false), and its Kernel catches the exception and renders it through the app's exception handler. This ticket goes one step further than Laravel by also owning the command base class, so that the command concept (name, description, help, arguments, options, exec()) is Quantum's own and not a thin layer over Symfony's.
Current state (what depends on Symfony Console)
11 files in src/ import Symfony Console (14 distinct classes) and 23 test files.
| Symfony class | Used by |
|---|---|
Application |
RunCommandStage, SetupConsoleApplicationStage, InitConsoleStage, RegisterCoreCommandsStage, RegisterAppCommandsStage, ResolveEnvironmentStage, ConsoleContext, CommandRegistrar, InstallToolkitCommand (getApplication()->find()) |
Command |
base class of src/Console/CliCommand.php |
InputInterface, ArgvInput, ArrayInput, InputArgument, InputOption |
InitConsoleStage, ConsoleContext, ResolveEnvironmentStage, CliCommand, InstallToolkitCommand |
OutputInterface, ConsoleOutput, NullOutput |
InitConsoleStage, ConsoleContext, CliCommand, InstallToolkitCommand, src/Tracer/ErrorHandler.php |
Table |
src/Console/Commands/RouteListCommand.php |
QuestionHelper, ConfirmationQuestion |
CliCommand::confirm() |
Exception\ExceptionInterface |
InstallToolkitCommand |
Command-side API actually used (all 13 core commands and the 10 commands of the quantum/project skeleton): exec(), the declared properties ($name, $description, $help, $args, $options, $requiresEnvironment), and getArgument(), getOption(), output(), info(), error(), comment(), question(), confirm(). Only InstallToolkitCommand (core) and DemoCommand (skeleton) use getApplication()->find($name)->run(new ArrayInput(...), new NullOutput()) to run another command. The skeleton's UserShowCommand, PostShowCommand and DemoCommand use Table and ProgressBar on $this->output.
Scope
In scope
- A native
Quantum\Console\Application. - A rewritten
Quantum\Console\CliCommandthat no longer extends SymfonyCommand, and an extendedQuantum\Console\Contracts\CommandInterface. - Native
listandhelpcommands. - Global flags:
-q/--quiet,-v/-vv/-vvv,-n/--no-interaction,--ansi/--no-ansi,-h/--help. - Changes to the console stages and
ConsoleContextso the runtime owns the exit code and the error path. - A Quantum command tester shipped in
src/to replace Symfony'sCommandTester. - Command-name abbreviations (
mig:gen), see below. - Tests,
CHANGELOG.mdBREAKINGentry with the migration note.
Out of scope (see Follow-ups): shell completion; command aliases.
Design
Quantum\Console\Application
- Holds the registered commands by name:
add(),has(),get(),find(),all(). find($name): exact match first; otherwise resolve an abbreviation the way Symfony does (each:-separated segment is a prefix:mig:genfindsmigration:generate). An ambiguous abbreviation throws a clear exception listing the candidates; an unknown name throws "Commandxis not defined" (keep the current message).run(InputInterface, OutputInterface): int:- apply the global flags to the input and output;
- read the command name (
getFirstArgument()); no name orlistrenders the list;help <name>or--helprenders the help of a command; - otherwise
find($name)->run($input, $output)and return its exit code.
- It does not call
exit()and does not catch exceptions: a command's exception reachesRunCommandStageand the pipeline, and from there the QuantumErrorHandler, as in the web app.ConsoleAppAdapter::start()keeps returning the exit code toqt, which callsexit(). - Name and version (
SetupConsoleApplicationStage) are kept and used by the list and--version.
Quantum\Console\CliCommand and CommandInterface
- No longer extends
Symfony\Component\Console\Command\Command. - Keeps the declaration style exactly as it is:
$name,$description,$help,$args,$options,$requiresEnvironment,exec(). Existing core and skeleton commands need no change. - Keeps
getArgument(),getOption(),output(),info(),comment(),question(),error(),confirm(),requiresEnvironment(), and the$inputand$outputproperties (sonew Table($this->output)keeps working). - Adds
getName(),getDescription(),getHelp(),getDefinition()(anInputDefinitionbuilt from$argsand$options, the mapping code inCliCommand::setArguments()andsetOptions()moves here),getApplication()(for chaining), andrun(InputInterface, OutputInterface): int. run()builds the full definition (the command-name argument first, the global options, then the command's own), binds and validates the input, sets$inputand$output, callsexec()and returns the exit code. Binding errors (missing argument, unknown option) surface as Symfony's input exceptions with their existing messages.CommandInterfacegains the methods above so thatCommandRegistrarand the application depend on the interface and not on a class.
list and help
- Native commands with
requiresEnvironment = false, rendered from the command definition (name, description, usage line fromInputDefinition::getSynopsis(), arguments, options, help text), using the SymfonyTablehelper for the list. - This removes the "Symfony's
list/helpdo not need the environment" special case fromResolveEnvironmentStage.
Stages and context
InitConsoleStage: register the QuantumApplicationinstead of the Symfony one; the input and output registrations stay.ConsoleContext::getApplication()returns the QuantumApplication.RegisterCoreCommandsStage,RegisterAppCommandsStage,CommandRegistrar: type changes only.ResolveEnvironmentStage: look the command up in the Quantum application; drop thelist/helpspecial case.RunCommandStage: call the Quantum application. Theconsole.command.before/afterevents keep their names. Re-check whetherconsole.command.aftercan now fire in a real run (it could not with Symfony'sexit()), and updateCLAUDE.md/the changelog andPipelineTestaccordingly.- Exit code: a command's
exec()returns nothing today; keep0on success. A thrown exception ends the run through the pipeline and the error handler; decide and document the exit code for that case (non-zero).
Command tester
- Add
Quantum\Console\Testing\CommandTesterinsrc/(nottests/) so apps can test their own commands with it. Same methods as Symfony's:execute(array $input),getDisplay(),getStatusCode(),setInputs(array)(answers forconfirm()via an in-memory input stream),assertCommandIsSuccessful(). - Built on the Symfony
ArrayInputandStreamOutputthat stay in the project; it adds the command name argument automatically. - The 14 existing test files that use Symfony's
CommandTester(50 uses) switch to it with a one-lineusechange.tests/Helpers/NonExitingApplication.phpis removed.
Compatibility and migration
Breaking for apps and packages that:
- call Symfony
Commandmethods on$thisinside their ownCliCommandsubclasses (getHelper(),addArgument(),addOption(),getDefinition()as a Symfony definition,setHidden(), and so on); - type-hint or resolve
Symfony\Component\Console\Applicationfrom the container, or useConsoleContext::getApplication()as a Symfony application; - test their commands with Symfony's
CommandTester.
Not affected: commands that only use exec(), the declared properties and the helper methods listed above, which is the documented way. The quantum/project skeleton's commands fall in this group; its DemoCommandTest uses Symfony's CommandTester and must be moved to the Quantum tester.
CHANGELOG.md gets a BREAKING entry with the migration note for apps and custom commands (what to replace, how to declare the same command, how to switch tests).
Implementation plan (one PR, several commits)
- Native
Applicationand thelistandhelpcommands, with tests. - Rewritten
CliCommandandCommandInterface, with tests. - Stages,
ConsoleContext,CommandRegistrar,InstallToolkitCommand; simplifyResolveEnvironmentStage. - Quantum
CommandTester; migrate the existing tests; removeNonExitingApplication. CHANGELOG.md,CLAUDE.mdarchitecture notes (local, not committed), documentation ticket.
Feasibility
A throwaway prototype (outside the repository) of a native application, a native command base declared the Quantum way and a native tester passed 16 checks on PHP 8.1 against the real Symfony input, output and helper classes: ArgvInput with arguments, shortcuts, array options and flags; missing-argument and unknown-option errors; an exception thrown inside a command reaching the caller; -q, -v, -n and --ansi; confirm() with a standalone QuestionHelper; Table and ProgressBar on $this->output; chaining with find()->run(ArrayInput, NullOutput); abbreviations; list and help from the definition; the tester. Two details to carry into the implementation: the command name has to be the first argument of every command's definition, and the global options have to be defined on every command, otherwise binding rejects them.
Still to verify during implementation (not proven by the prototype): the help <command> form; a real run through php qt with the boot stages and ErrorHandler; PHP 8.0 and 8.2 (CI matrix); the exit code after an exception.
Acceptance criteria
php qt <command>runs through the native application; arguments, options, shortcuts, array arguments/options and flags behave as before.- A missing argument or an unknown option gives a clear error and a non-zero exit code.
- An exception in a command reaches the
ErrorHandlerand is rendered like other errors; noexit()inside the framework;start()returns the exit code. list,help,help <command>and<command> --helpwork and do not require the environment.-q,-v/-vv/-vvv,-nand--ansi/--no-ansiwork.- Abbreviations resolve when unambiguous and give a clear error when ambiguous or unknown.
confirm()works interactively, with-n, and in tests withsetInputs().InstallToolkitCommandcan still run another command.- No file in
src/importsSymfony\Component\Console\ApplicationorSymfony\Component\Console\Command\Command. ResolveEnvironmentStagehas no special case forlist/help.- The Quantum
CommandTesterexists, is documented, and all existing command tests use it. composer cs:check,composer phpstan(level 8) and the PHPUnit suite pass; CI passes on PHP 8.0, 8.1 and 8.2.
Follow-ups (separate tickets, not blocking this one)
- Shell completion. Symfony's
Applicationshipscompletionand_completetoday. The native application needs acompletioncommand that prints the shell script (Symfony's bash, zsh and fish scripts invendor/symfony/console/Resourcescan be reused as files) and a_completecommand built onCompletionInputandCompletionSuggestions, which work without anApplication. Include per-command value suggestions. (Do it in this ticket if it turns out to be cheap.) - Command aliases.
- Starter project (
quantum/project): moveDemoCommandTestto the Quantum tester and adjust anything that resolves the Symfony application. - Documentation (
quantum-php/docs): console and command pages, testing commands, the migration note, the upgrade guide.
Activity