Skip to content

feat: introduce modern attribute-based spark commands#10120

Open
paulbalandan wants to merge 11 commits intocodeigniter4:4.8from
paulbalandan:refreshed-commands
Open

feat: introduce modern attribute-based spark commands#10120
paulbalandan wants to merge 11 commits intocodeigniter4:4.8from
paulbalandan:refreshed-commands

Conversation

@paulbalandan
Copy link
Copy Markdown
Member

Description

Introduces a new modern spark command system alongside the existing BaseCommand. Modern commands describe themselves through a #[Command] attribute, build their argument/option surface inside a configure() method using readonly value objects (Argument, Option), and implement execute(array $arguments, array $options): int. The framework parses the command line, applies declared defaults, validates the input, and hands the result to execute().

The legacy BaseCommand style 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. BaseCommand will 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 BaseCommand while "modern" commands are those extending AbstractCommand.

Benefits / enhancements:

  • Declarative identity. #[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.
  • Type-safe definitions. Argument and Option are final readonly value objects whose invariants are enforced in the constructor. Configuration mistakes (required argument with a default, array option without requiresValue, negatable option accepting a value, …) throw a typed InvalidArgumentDefinitionException / InvalidOptionDefinitionException at the point of declaration.
  • Automatic bind & validate. Command authors stop hand-parsing $params. The framework binds raw tokens to declared arguments/options, applies defaults, coerces flags, handles --name / -n / --no-name aliasing, and rejects extraneous or missing input before execute() runs.
  • Explicit lifecycle. configure()initialize()interact() → bind & validate → execute(). Each hook has one job, and prior-phase mutations flow cleanly into the next phase.
  • Alias-aware input helpers. hasUnboundOption() / getUnboundOption() let interact() ask "was this option passed?" without knowing whether the user typed the long name, the shortcut, or the negation form.
  • Unbound vs. validated state. Authors can cleanly distinguish "the user actually passed this" from "the framework resolved this to its declared default" — useful when forwarding to other commands or auditing behaviour.
  • Typed, targeted exceptions. ArgumentCountMismatchException, OptionValueMismatchException, UnknownOptionException, and CommandNotFoundException replace opaque RuntimeException usage and give callers something meaningful to catch.
  • Safer exception rendering. 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 AbstractCommand in this PR: help, list, cache:clear, debugbar:clear, logs:clear, and serve. 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 / execute and 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:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@paulbalandan paulbalandan added the new feature PRs for new features label Apr 18, 2026
@github-actions github-actions bot added the 4.8 PRs that target the `4.8` branch. label Apr 18, 2026
Copy link
Copy Markdown
Member

@michalsn michalsn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread system/CLI/Console.php
Comment thread system/Commands/Help.php
Comment thread system/CLI/AbstractCommand.php Outdated
@paulbalandan
Copy link
Copy Markdown
Member Author

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?

@michalsn
Copy link
Copy Markdown
Member

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
(1) I would lean toward documenting this as a potential BC break

@paulbalandan paulbalandan added the breaking change Pull requests that may break existing functionalities label Apr 18, 2026
@paulbalandan paulbalandan requested a review from Copilot April 19, 2026 04:11
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, typed Argument/Option definitions, 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 -h BC).

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.

Comment on lines +823 to +824
if ($definition->requiresValue && ! is_string($value) && ! is_array($value)) {
throw new OptionValueMismatchException(lang('Commands.optionRequiresValue', [$name]));
Comment on lines +67 to +70
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]),
Comment thread system/CLI/Console.php
* @internal
*/
public const DEFAULT_COMMAND = 'list';
private const DEFAULT_COMMAND = 'list';
Comment on lines +40 to +42
->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'));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.8 PRs that target the `4.8` branch. breaking change Pull requests that may break existing functionalities new feature PRs for new features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants