Skip to content

[Symfony61] Do not add #[AsCommand] without a name in CommandConfigureToAttributeRector - #1102

Open
kix wants to merge 1 commit into
rectorphp:mainfrom
kix:fix/command-configure-to-attribute-require-name
Open

kix wants to merge 1 commit into
rectorphp:mainfrom
kix:fix/command-configure-to-attribute-require-name

Conversation

@kix

@kix kix commented Nov 2, 2026

Copy link
Copy Markdown

CommandConfigureToAttributeRector moves setName(), setDescription(), setAliases() and setHidden() out of configure() into #[AsCommand(...)]. It never checks that a name ends up in the attribute.

AsCommand::__construct() takes a required string $name as its first argument. When a command gets its name from somewhere else, for example the constructor:

final class SomeCommand extends Command
{
    public function __construct(?string $name = 'app:some')
    {
        parent::__construct($name);
    }

    protected function configure(): void
    {
        $this->setDescription('Some description');
    }
}

the rule currently produces #[AsCommand(description: 'Some description')]. Compiling the container (or booting the console) then fails with:

ArgumentCountError: Symfony\Component\Console\Attribute\AsCommand::__construct(): Argument #1 ($name) not passed

We hit this running the Symfony 6.0-7.2 sets on a Symfony 7.4 app: two commands got a nameless attribute and every bin/console call crashed.

Change

Before extracting anything, require that a name will be there: either already on an existing #[AsCommand] (name:) or set by a constant setName() in configure(). If there is something to move into the attribute but no name, the class is left untouched (same approach as the existing skip for non-constant values). configure() bodies with nothing to move, like empty_configure_body, still behave as before.

Tests

New fixtures:

  • skip_name_passed_to_parent_constructor: the case above, left unchanged. Fails without the change.
  • skip_non_constant_name: setName($this->commandName) next to a constant description, left unchanged. Fails without the change.
  • description_with_name_on_existing_attribute: #[AsCommand(name: ...)] plus a description in configure() still converts (regression guard).

The rule's tests pass (17), plus ECS and PHPStan.

🤖 Generated with Claude Code

…eToAttributeRector

#[AsCommand] requires `name`. When configure() only sets a description, aliases or
hidden, and the name is not on the attribute nor set by a constant setName() (it is
passed to the parent constructor, for example), the rule created
#[AsCommand(description: '...')], which throws ArgumentCountError when the container
or the console boots. Leave such classes untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
if (! $node instanceof MethodCall || ! $this->isName($node->name, $methodName)) {
return null;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

early check :

if ($node->isFirstClassCallable()) {
    return null;
}

here ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants