Skip to content

AnnotationToAttributeRector: fold unknown named args into the attribute's $options - #8914

Merged
TomasVotruba merged 1 commit into
rectorphp:mainfrom
kix:fix/annotation-to-attribute-unknown-named-args
Nov 3, 2026
Merged

TomasVotruba merged 1 commit into
rectorphp:mainfrom
kix:fix/annotation-to-attribute-unknown-named-args

Conversation

@kix

@kix kix commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Problem

AnnotationToAttributeRector turns every annotation argument into a named attribute argument. If the attribute constructor has no parameter of that name, PHP throws Unknown named parameter the moment the attribute is instantiated - while the annotation form was accepted.

I hit this migrating a Symfony 5.4 app. On 5.4 Length::__construct() has no allowEmptyString parameter, the setting is only reachable through $options:

/** @Assert\Length(min=1, max=400, allowEmptyString=true) */
// becomes
#[Assert\Length(min: 1, max: 400, allowEmptyString: true)]   // Error: Unknown named parameter $allowEmptyString

The error only shows up when the constraint metadata for that class is loaded, so it passes lint and static analysis.

Change

New UnknownNamedArgsToOptionsFolder, called from PhpAttributeGroupFactory::create(). When the attribute class is resolvable, its constructor has no parameter with a given name, and it has an array-accepting $options parameter, the unknown arguments are folded into it:

#[Assert\Length(min: 1, max: 400, options: ['allowEmptyString' => true])]

It is deliberately conservative and leaves the arguments untouched when:

  • the attribute class cannot be resolved (so behaviour is unchanged where e.g. the vendor class is not installed - this is also why the existing Symfony fixtures are unaffected)
  • the constructor has no $options parameter, or is variadic
  • the argument is positional
  • an explicit options: argument exists and is not an array literal

The check uses the project's installed class, so the result follows the installed Symfony version: on versions where the constructor does declare the parameter, it stays a named argument.

Tests

Three fixtures with stub attribute classes (Symfony's validator is not installed in this repo): unknown arg folded, known args stay named, no $options parameter left alone. The first fails without the change.

vendor/bin/phpunit passes (4874 tests) and ECS is clean. PHPStan reports the same 17 errors on main with and without this change, none in touched files. I did not run it against the real Symfony 5.4 Length class, only against the stubs.

Happy to adjust the approach if you would rather solve this differently (e.g. in rector-symfony).

🤖 Generated with Claude Code

…te's $options

Annotation arguments become named attribute arguments. When the attribute
constructor does not declare a parameter of that name, PHP throws "Unknown
named parameter" as soon as the attribute is instantiated, while the annotation
form was accepted.

This hits Symfony validator constraints on versions where a setting is only
reachable through the $options array, e.g. on Symfony 5.4:

    @Assert\Length(min=1, allowEmptyString=true)
    -> #[Assert\Length(min: 1, allowEmptyString: true)]   // Error on load

Now, when the attribute class is resolvable, its constructor has no parameter of
that name, and it has an array-accepting $options parameter, the unknown
arguments are folded into it:

    #[Assert\Length(min: 1, options: ['allowEmptyString' => true])]

Anything else is untouched: unresolvable classes, constructors without
$options, variadic constructors, positional args, and an explicit non-array
options argument.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@TomasVotruba
TomasVotruba merged commit 952de48 into rectorphp:main Nov 3, 2026
51 checks passed
@TomasVotruba

Copy link
Copy Markdown
Member

Thank you

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.

3 participants