Skip to content

Resolve container binding arguments by name in ContainerBindConcreteWithClosureOnlyRector - #566

Open
chinmaypurav wants to merge 1 commit into
driftingly:mainfrom
chinmaypurav:fix-container-bind-named-and-shared-arguments
Open

Resolve container binding arguments by name in ContainerBindConcreteWithClosureOnlyRector#566
chinmaypurav wants to merge 1 commit into
driftingly:mainfrom
chinmaypurav:fix-container-bind-named-and-shared-arguments

Conversation

@chinmaypurav

Copy link
Copy Markdown
Contributor

Fixes #397.

Named arguments produced fatal code

The rule read the abstract and concrete arguments positionally and then dropped index 0, so a call using named arguments lost $abstract:

 $this->app->singleton(
-    abstract: SomeClass::class,
-    concrete: function (Application $app, array $parameters) {
+    concrete: function (Application $app, array $parameters): SomeClass {
         // ...
     },
 );

Container::singleton($abstract, $concrete = null) still requires $abstract, so the result is an ArgumentCountError.

Arguments are now matched to parameters by Arg::$name when present and by position otherwise. That also picks up the reversed form, singleton(concrete: $fn, abstract: SomeClass::class), which the old positional read skipped entirely. The closure has its name stripped when it moves into the abstract position, so the output is plain singleton($fn).

$shared was silently dropped

Not part of the issue, but found while writing fixtures for it. bind(SomeClass::class, $fn, true) became bind($fn, true). Laravel routes a closure abstract through bindBasedOnClosureReturnTypes($abstract, $concrete, $shared), which overwrites $concrete with the closure — so the true landed in $concrete and was discarded, quietly turning a shared binding into a non-shared one. It is now emitted as bind($fn, shared: true).

bindIf() / singletonIf() removed from the rule

Also outside the issue, and the largest behavioral change here. Both forward the abstract to bound(), which does isset($this->bindings[$abstract]). With a closure abstract that throws TypeError: Cannot access offset of type Closure in isset or empty — neither method has the closure handling that bind() gained. So every transform this rule made on bindIf/singletonIf produced code that fatals at boot, named arguments or not. Happy to split this into its own PR if you'd rather review it separately.

Minor

Removed the $classString instanceof Const_ guard. Arg::$value is always an Expr and Const_ extends NodeAbstract, so it could never be true; PHPStan flags it as instanceof.alwaysFalse once the argument type is narrowed. No behavior change.

Tests

New fixtures: fixture_named_arguments, fixture_shared_argument, skip_conditional_binding, skip_spread_arguments.

PHPStan (level max), duster lint, rector --dry-run, and structarmed are clean. The full suite goes 673 → 677 tests with the same 1 error / 14 failures that already fail on main, all in unrelated rules.

🤖 Generated with Claude Code

ContainerBindConcreteWithClosureOnlyRector read the abstract and concrete
arguments positionally, so named arguments produced invalid code:

    $app->singleton(abstract: Foo::class, concrete: fn () => new Foo())

became `$app->singleton(concrete: fn () => new Foo())`, which fatals with
an ArgumentCountError because $abstract is required. Arguments are now
matched to parameters by name where present and by position otherwise, so
reversed named arguments resolve correctly too, and the closure loses its
name when it moves into the abstract position.

The $shared argument was silently dropped as well. Laravel routes a
closure abstract through bindBasedOnClosureReturnTypes(), which overwrites
$concrete, so `bind(Foo::class, $fn, true)` became `bind($fn, true)` and
stopped being shared. It is now emitted as `bind($fn, shared: true)`.

Drop bindIf() and singletonIf() from the rule entirely. Both pass the
abstract to bound(), which uses it as an array offset, so a closure
abstract throws "Cannot access offset of type Closure in isset or empty".
Every transform the rule made on those two methods produced code that
fatals at boot.

Also remove the `$classString instanceof Const_` guard: Arg::$value is
always an Expr and Const_ extends NodeAbstract, so it could never be true.

Fixes driftingly#397

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ContainerBindConcreteWithClosureOnlyRector Rule creates invalid code with named arguments

1 participant