Skip to content

refactor!: modernize console commands with attributes#6527

Merged
gharlan merged 5 commits into
6.xfrom
console-command-attributes
May 30, 2026
Merged

refactor!: modernize console commands with attributes#6527
gharlan merged 5 commits into
6.xfrom
console-command-attributes

Conversation

@gharlan
Copy link
Copy Markdown
Member

@gharlan gharlan commented May 30, 2026

Summary

Modernizes the console command layer to use Symfony 8.1 attributes.

  • Attribute-based registration & discovery. Commands are registered via Symfony's native #[AsCommand] attribute and discovered through ClassDiscovery (core + active addons). The console_commands property in package.yml is gone — an addon now registers a command simply by adding #[AsCommand] to a class extending AbstractCommand, the same way #[AsExtension] already works.
  • Invokable commands. configure()/execute() are replaced by __invoke() with #[Argument]/#[Option] parameters and injected SymfonyStyle/OutputInterface. Completion suggestions are inline static function closures / first-class callables right on the attributes (PHP 8.5 allows static closures and first-class callables in constant expressions).
  • Lazy command loading. The CommandLoader returns LazyCommands, so listing commands (console list, shell completion) no longer instantiates every command class — only the command that is actually executed is built. This matters most for addon-provided commands.
  • AbstractCommand::$addon is now a lazily-resolved, non-nullable property (derived from the command class location), replacing getAddon()/setAddon(). Reading it on a core command throws.
  • Setup gating is expressed via the new AvailableInSetupInterface marker, replacing the hardcoded list of setup-time commands in the loader.
  • Command classes are now final.

Also fixes a pre-existing 6.x regression in cronjob:run --job: running it without a value is documented to let you pick a job interactively, but an (int) cast turned the value-less null into 0, so the interactive picker was unreachable. 5.x behaves correctly; this restores that behavior.

Breaking changes

  • The console_commands property in package.yml is removed — use #[AsCommand] on the command class instead.
  • AbstractCommand::getAddon() / setAddon() are removed in favor of the $addon property. The Rector upgrade rules are updated accordingly (getPackage()/getAddon()->addon).

gharlan added 4 commits May 29, 2026 19:42
Console commands are now registered via Symfony's native #[AsCommand]
attribute and discovered through ClassDiscovery (core + addons), instead
of the hardcoded loader map and the "console_commands" package.yml property.

- Remove the "console_commands" package.yml property (and its JSON schema)
- Add #[AsCommand] (name/description/help) to all core commands
- Add AvailableInSetupInterface to gate which commands show during setup
- CommandLoader returns LazyCommand, so "console list" no longer instantiates
  every command - only the executed command is instantiated
- AbstractCommand::$addon is now a lazily resolved, non-nullable property
  (replaces getAddon()/setAddon()); reading it on a core command throws
- Update rector upgrade rules accordingly
Replace configure()/execute() with Symfony 8.1 invokable commands: arguments
and options are declared via #[Argument]/#[Option] attributes on the __invoke()
parameters, with SymfonyStyle/OutputInterface injected directly. The generated
input definitions (names, modes, defaults, shortcuts, descriptions) are
unchanged.

Completion suggestions are inline `static function` closures right on the
attributes (PHP 8.5 allows static closures in constant expressions) - the same
inline style the commands used in configure() before, so no separate public
suggest* helper methods are needed.

AssetsSyncCommand keeps configure() for its runtime-computed help text. The
now-unused AbstractCommand::getStyle() helper is removed.

Also fixes a pre-existing missing space in UserListCommand's completion query
("SELECT login FROM" . table -> "FROM " . table) that produced invalid SQL.
Running "cronjob:run --job" without a value is documented to let you pick a job
interactively, but since 6.x it errored out instead: the option value was cast
with (int), turning the value-less null into 0, so executeSingleJob() never
received null and the interactive ChoiceQuestion branch was unreachable.

Use the typed bool|string option value directly - a value-less --job resolves
to true, which now maps to null (interactive selection); a given id is cast to
int as before. Restores the 5.x behavior.
The concrete command classes are @internal leaf classes that are instantiated
via the command loader, never extended. Marking them final makes that explicit,
finishes what UserListCommand/UserDeleteCommand already started, and clears the
ClassMustBeFinal hints. AbstractCommand stays abstract.
@gharlan gharlan added this to the REDAXO 6.0 milestone May 30, 2026
The config:get/set argument was renamed from "config-key" to "key" (a breaking
change; positional invocation is unaffected). Update the command tests to pass
the argument under its new name.
@gharlan gharlan force-pushed the console-command-attributes branch from fb887ff to 9bd53dc Compare May 30, 2026 07:36
@gharlan gharlan merged commit 6b7a3a1 into 6.x May 30, 2026
16 checks passed
@gharlan gharlan deleted the console-command-attributes branch May 30, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

2 participants