feat: introduce modern attribute-based spark commands#10120
feat: introduce modern attribute-based spark commands#10120paulbalandan wants to merge 11 commits intocodeigniter4:4.8from
spark commands#10120Conversation
319425c to
a944b89
Compare
michalsn
left a comment
There was a problem hiding this comment.
I think this is a solid direction overall.
One thing worth adding is a short migration section: which old properties map to which attributes or configure() calls, that run() becomes execute(), and how to swap raw $params and CLI::getOption() for the new validated arguments and options. That's the gap someone with a dozen existing commands will hit on day one.
|
Thanks! Another question I want to pop is: how comfortable are we to migrate freely the built in commands? Since they are not final, theoretically any user can extend them and provide an overriding implementation. Do we (1) mark this PR as potentially BC breaking? or (2) do not allow extensions and advise them to use composition instead? |
Hmm... I totally forgot that users may extend our commands. Although I never did that myself. (2) It's a bit too late for that now. People may already extend these commands |
There was a problem hiding this comment.
Pull request overview
Introduces a new attribute-based “modern” Spark command system (alongside legacy BaseCommand) and migrates several built-in commands and tests/documentation to the new lifecycle, binding, and validation model.
Changes:
- Added modern command framework pieces (
#[Command],AbstractCommand, typedArgument/Optiondefinitions, and new CLI exceptions) and updated discovery/execution to support dual registries. - Migrated built-in commands (
help,list,cache:clear,debugbar:clear,logs:clear,serve) to the modern API and adjusted console routing (--help/-h, legacy vs modern execution). - Updated user guide, changelog, fixtures, and system tests to cover modern/legacy coexistence and behavior changes (e.g., removing
routes -hBC).
Reviewed changes
Copilot reviewed 71 out of 72 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| utils/src/PhpCsFixer/CodeIgniterRuleCustomisationPolicy.php | Updates fixer exclusions for renamed legacy fixtures path. |
| utils/phpstan-baseline/missingType.iterableValue.neon | Removes baseline entries now resolved by refactors. |
| user_guide_src/source/cli/index.rst | Adds modern commands guide to CLI docs index. |
| user_guide_src/source/cli/cli_modern_commands.rst | New documentation for modern command authoring and lifecycle. |
| user_guide_src/source/cli/cli_modern_commands/001.php | Minimal modern command example. |
| user_guide_src/source/cli/cli_modern_commands/002.php | Modern argument definition examples. |
| user_guide_src/source/cli/cli_modern_commands/003.php | Modern option definition examples. |
| user_guide_src/source/cli/cli_modern_commands/004.php | interact() and alias-aware option helpers example. |
| user_guide_src/source/cli/cli_modern_commands/005.php | Validated vs unbound accessors example. |
| user_guide_src/source/cli/cli_modern_commands/006.php | Calling another modern command example. |
| user_guide_src/source/cli/cli_modern_commands/007.php | Additional usage examples via addUsage(). |
| user_guide_src/source/cli/cli_modern_commands/008.php | Forwarding unbound input to another command example. |
| user_guide_src/source/cli/cli_modern_commands/009.php | Legacy BaseCommand example used for migration docs. |
| user_guide_src/source/cli/cli_modern_commands/010.php | Modern equivalent example (attribute + configure/execute). |
| user_guide_src/source/changelogs/v4.8.0.rst | Documents behavior changes, enhancements, and deprecations related to commands. |
| tests/system/Commands/Utilities/RoutesTest.php | Removes test for deprecated routes -h behavior. |
| tests/system/Commands/Translation/LocalizationFinderTest.php | Updates tests to use runLegacy() for legacy command invocation. |
| tests/system/Commands/HelpCommandTest.php | Rewrites assertions for modern help output and legacy help delegation. |
| tests/system/Commands/ConfigurableSortImportsTest.php | Updates fixture paths for renamed legacy command support files. |
| tests/system/Commands/Cache/ClearCacheTest.php | Updates expected error message for migrated cache clear command. |
| tests/system/CLI/SignalTest.php | Updates imports for legacy fixture namespace changes. |
| tests/system/CLI/Input/OptionTest.php | New tests for Option value object invariants and validation. |
| tests/system/CLI/Input/ArgumentTest.php | New tests for Argument value object invariants and validation. |
| tests/system/CLI/ConsoleTest.php | Adds coverage for help-option routing and legacy execution path. |
| tests/system/CLI/CommandsTest.php | Adds dual-registry discovery/execution tests and new deprecation/exception behaviors. |
| tests/system/CLI/BaseCommandTest.php | Updates legacy fixture namespace imports. |
| tests/system/CLI/Attributes/CommandTest.php | New tests for #[Command] attribute validation rules. |
| tests/system/CLI/AbstractCommandTest.php | Comprehensive tests for modern command lifecycle, binding, and validation. |
| tests/_support/_command/DuplicateModern.php | Modern fixture to test duplicate-name discovery warning behavior. |
| tests/_support/_command/DuplicateLegacy.php | Legacy fixture to test duplicate-name discovery warning behavior. |
| tests/_support/_command/AppInfo.php | Renames override fixture to target legacy app:info instead of list. |
| tests/_support/_command/AppAboutCommand.php | Modern override fixture for app:about. |
| tests/_support/InvalidCommands/NoAttributeCommand.php | Fixture for discovery skipping modern commands without #[Command]. |
| tests/_support/InvalidCommands/EmptyCommandName.php | Fixture for discovery logging when attribute instantiation fails. |
| tests/_support/Commands/Modern/TestFixtureCommand.php | Modern fixture command for registry/lifecycle tests. |
| tests/_support/Commands/Modern/InteractFixtureCommand.php | Fixture to test interact() mutations flowing into bind/validate/execute. |
| tests/_support/Commands/Modern/AppAboutCommand.php | Fixture covering binding/validation/accessor behaviors. |
| tests/_support/Commands/Legacy/Unsuffixable.php | Moves legacy fixtures under Tests\\Support\\Commands\\Legacy namespace. |
| tests/_support/Commands/Legacy/SignalCommandNoPosix.php | Moves legacy fixtures under Tests\\Support\\Commands\\Legacy namespace. |
| tests/_support/Commands/Legacy/SignalCommandNoPcntl.php | Moves legacy fixtures under Tests\\Support\\Commands\\Legacy namespace. |
| tests/_support/Commands/Legacy/SignalCommand.php | Moves legacy fixtures under Tests\\Support\\Commands\\Legacy namespace. |
| tests/_support/Commands/Legacy/NullReturningCommand.php | New legacy fixture to test deprecation for null exit codes. |
| tests/_support/Commands/Legacy/LanguageCommand.php | Moves legacy fixtures under Tests\\Support\\Commands\\Legacy namespace. |
| tests/_support/Commands/Legacy/InvalidCommand.php | Improves thrown exception message for invalid legacy fixture. |
| tests/_support/Commands/Legacy/HelpLegacyCommand.php | Legacy fixture to test legacy→modern invocation via runCommand(). |
| tests/_support/Commands/Legacy/Foobar.php | Moves legacy fixture file into Commands/Legacy directory. |
| tests/_support/Commands/Legacy/DestructiveCommand.php | Moves legacy fixtures under Tests\\Support\\Commands\\Legacy namespace. |
| tests/_support/Commands/Legacy/AppInfo.php | Updates legacy fixture behavior to route through legacy help fixture. |
| tests/_support/Commands/Legacy/AbstractInfo.php | Moves legacy fixtures under Tests\\Support\\Commands\\Legacy namespace. |
| system/Language/en/Commands.php | Adds new language strings for modern command validation/errors. |
| system/Language/en/CLI.php | Adds helpAvailableCommands label used by modern list output. |
| system/Commands/Utilities/Routes.php | Removes -h backward compatibility for --handler. |
| system/Commands/Server/Serve.php | Migrates serve to modern AbstractCommand + option definitions. |
| system/Commands/ListCommands.php | Migrates list to modern command and updates rendering logic. |
| system/Commands/Housekeeping/ClearLogs.php | Migrates logs:clear to modern command with interact() confirmation. |
| system/Commands/Housekeeping/ClearDebugbar.php | Migrates debugbar:clear to modern command. |
| system/Commands/Help.php | Migrates help to modern command with modern/legacy branching. |
| system/Commands/Cache/ClearCache.php | Migrates cache:clear to modern command and updates messaging. |
| system/CLI/Input/Option.php | Adds readonly Option definition value object with invariants. |
| system/CLI/Input/Argument.php | Adds readonly Argument definition value object with invariants. |
| system/CLI/Exceptions/UnknownOptionException.php | New typed exception for unknown options. |
| system/CLI/Exceptions/OptionValueMismatchException.php | New typed exception for option/value mismatches. |
| system/CLI/Exceptions/InvalidOptionDefinitionException.php | New typed exception for invalid option definitions. |
| system/CLI/Exceptions/InvalidArgumentDefinitionException.php | New typed exception for invalid argument definitions. |
| system/CLI/Exceptions/CommandNotFoundException.php | New typed exception for unknown commands (programmatic access). |
| system/CLI/Exceptions/ArgumentCountMismatchException.php | New typed exception for argument count mismatches. |
| system/CLI/Console.php | Routes legacy vs modern execution and improves --help handling. |
| system/CLI/Commands.php | Implements dual registries, modern discovery via attributes, and new run APIs. |
| system/CLI/BaseCommand.php | Updates legacy command-to-command calls to use runLegacy(). |
| system/CLI/Attributes/Command.php | Adds #[Command] attribute with validation. |
| system/CLI/AbstractCommand.php | Adds modern command base class, lifecycle, bind/validate, and helpers. |
| rector.php | Updates rector skip paths for moved fixtures. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if ($definition->requiresValue && ! is_string($value) && ! is_array($value)) { | ||
| throw new OptionValueMismatchException(lang('Commands.optionRequiresValue', [$name])); |
| foreach ([ | ||
| ...$this->getCommandRunner()->getCommands(), | ||
| ...$this->getCommandRunner()->getModernCommands(), | ||
| ] as $command => $details) { |
| CLI::write(sprintf( | ||
| '%s%s', | ||
| CLI::color($this->addPadding($command[0], 2, $maxPad), 'green'), | ||
| CLI::wrap($command[1]), |
| * @internal | ||
| */ | ||
| public const DEFAULT_COMMAND = 'list'; | ||
| private const DEFAULT_COMMAND = 'list'; |
| ->addOption(new Option(name: 'php', description: 'The PHP binary to use.', acceptsValue: true, default: PHP_BINARY)) | ||
| ->addOption(new Option(name: 'host', description: 'The host to serve on.', acceptsValue: true, default: 'localhost')) | ||
| ->addOption(new Option(name: 'port', description: 'The port to serve on.', acceptsValue: true, default: '8080')); |
Description
Introduces a new modern
sparkcommand system alongside the existingBaseCommand. Modern commands describe themselves through a#[Command]attribute, build their argument/option surface inside aconfigure()method using readonly value objects (Argument,Option), and implementexecute(array $arguments, array $options): int. The framework parses the command line, applies declared defaults, validates the input, and hands the result toexecute().The legacy
BaseCommandstyle continues to work: both registries are discovered side-by-side, and a warning is printed at discovery when the same name is claimed by both styles.BaseCommandwill remain supported until every built-in command is migrated, at which point it will begin emitting deprecation notices.In this PR, "legacy" commands are those extending
BaseCommandwhile "modern" commands are those extendingAbstractCommand.Benefits / enhancements:
#[Command(name, description, group)]replaces magic properties and is validated at attribute-construction time, so a malformed name fails at discovery rather than at the first invocation.ArgumentandOptionare final readonly value objects whose invariants are enforced in the constructor. Configuration mistakes (required argument with a default, array option withoutrequiresValue, negatable option accepting a value, …) throw a typedInvalidArgumentDefinitionException/InvalidOptionDefinitionExceptionat the point of declaration.$params. The framework binds raw tokens to declared arguments/options, applies defaults, coerces flags, handles--name/-n/--no-namealiasing, and rejects extraneous or missing input beforeexecute()runs.configure()→initialize()→interact()→ bind & validate →execute(). Each hook has one job, and prior-phase mutations flow cleanly into the next phase.hasUnboundOption()/getUnboundOption()letinteract()ask "was this option passed?" without knowing whether the user typed the long name, the shortcut, or the negation form.ArgumentCountMismatchException,OptionValueMismatchException,UnknownOptionException, andCommandNotFoundExceptionreplace opaqueRuntimeExceptionusage and give callers something meaningful to catch.renderThrowable()can be called from any command; it temporarily makes the shared request a CLI request for the render and restores the prior request afterwards.Migrated built-in commands
The following framework commands have been migrated to
AbstractCommandin this PR:help,list,cache:clear,debugbar:clear,logs:clear, andserve. The remaining built-ins will be migrated in follow-up PRs.Interactive mode
Currently, in this PR, commands are interactive (i.e., every command that expects completed inputs can prompt the user via
interact()). In a follow up PR, I am intending to make this configurable so that we can have a non-interactive mode.Acknowledgement
The command lifecycle —
configure/initialize/interact/executeand the bind-before-validate flow — is inspired by Symfony Console. The implementation and public surface are CodeIgniter-specific; the vocabulary and staging idea are the parts we borrowed.AI Disclosure
This PR body and the documentation are assisted by Claude Opus 4.7. Initial review was also AI-assisted. Changes are reviewed before being accepted.
Note
The changed files seems to be too much but in reality the bulk of the changes are from the renaming of the "legacy" command fixtures so that they are distinguishable from the "modern" ones.
Checklist: