From 762611fc833c8d31d7da2395efa7e73df00e6657 Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 09:49:19 +0100 Subject: [PATCH 01/14] wip --- .../Container/Attributes/BindWhen.php | 36 ++++++++++ src/Illuminate/Container/Container.php | 69 ++++++++++++++---- tests/Container/ContainerTest.php | 70 +++++++++++++++++++ 3 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 src/Illuminate/Container/Attributes/BindWhen.php diff --git a/src/Illuminate/Container/Attributes/BindWhen.php b/src/Illuminate/Container/Attributes/BindWhen.php new file mode 100644 index 000000000000..6a9bce73db04 --- /dev/null +++ b/src/Illuminate/Container/Attributes/BindWhen.php @@ -0,0 +1,36 @@ +concrete = $concrete; + $this->condition = $condition; + } +} diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 5633559a6859..5c6fa9b31ef3 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -6,6 +6,7 @@ use Closure; use Exception; use Illuminate\Container\Attributes\Bind; +use Illuminate\Container\Attributes\BindWhen; use Illuminate\Container\Attributes\Scoped; use Illuminate\Container\Attributes\Singleton; use Illuminate\Contracts\Container\BindingResolutionException; @@ -981,8 +982,7 @@ protected function getConcrete($abstract) return $this->bindings[$abstract]['concrete']; } - if ($this->environmentResolver === null || - ($this->checkedForAttributeBindings[$abstract] ?? false) || ! is_string($abstract)) { + if (($this->checkedForAttributeBindings[$abstract] ?? false) || ! is_string($abstract)) { return $abstract; } @@ -990,7 +990,7 @@ protected function getConcrete($abstract) } /** - * Get the concrete binding for an abstract from the Bind attribute. + * Get the concrete binding for an abstract from the BindWhen or Bind attributes. * * @param string $abstract * @return mixed @@ -1005,10 +1005,59 @@ protected function getConcreteBindingFromAttributes($abstract) return $abstract; } + // BindWhen attributes are evaluated first and, unlike Bind, do not depend on + // the container's environment. The first attribute whose condition returns + // true wins, letting a binding be selected from arbitrary runtime state. + $concrete = $this->getConcreteFromBindWhenAttributes($reflected); + + if ($concrete === null && $this->environmentResolver !== null) { + $concrete = $this->getConcreteFromBindAttributes($reflected); + } + + if ($concrete === null) { + return $abstract; + } + + match ($this->getScopedTyped($reflected)) { + 'scoped' => $this->scoped($abstract, $concrete), + 'singleton' => $this->singleton($abstract, $concrete), + null => $this->bind($abstract, $concrete), + }; + + return $this->bindings[$abstract]['concrete']; + } + + /** + * Resolve the concrete for the first BindWhen attribute whose condition passes. + * + * @param ReflectionClass $reflected + * @return class-string|null + */ + protected function getConcreteFromBindWhenAttributes(ReflectionClass $reflected) + { + foreach ($reflected->getAttributes(BindWhen::class) as $reflectedAttribute) { + $instance = $reflectedAttribute->newInstance(); + + if (($instance->condition)($this)) { + return $instance->concrete; + } + } + + return null; + } + + /** + * Resolve the concrete from the Bind attributes for the current environment. + * + * @param ReflectionClass $reflected + * @return class-string|null + */ + protected function getConcreteFromBindAttributes(ReflectionClass $reflected) + { $bindAttributes = $reflected->getAttributes(Bind::class); if ($bindAttributes === []) { - return $abstract; + return null; } $concrete = $maybeConcrete = null; @@ -1033,17 +1082,7 @@ protected function getConcreteBindingFromAttributes($abstract) $concrete = $maybeConcrete; } - if ($concrete === null) { - return $abstract; - } - - match ($this->getScopedTyped($reflected)) { - 'scoped' => $this->scoped($abstract, $concrete), - 'singleton' => $this->singleton($abstract, $concrete), - null => $this->bind($abstract, $concrete), - }; - - return $this->bindings[$abstract]['concrete']; + return $concrete; } /** diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 36f1cc03a440..37606127bac5 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -4,6 +4,7 @@ use Attribute; use Illuminate\Container\Attributes\Bind; +use Illuminate\Container\Attributes\BindWhen; use Illuminate\Container\Attributes\Scoped; use Illuminate\Container\Attributes\Singleton; use Illuminate\Container\Container; @@ -875,6 +876,34 @@ public function testFlushResetsEnvironmentResolverAndCheckedBindings(): void $this->assertInstanceOf(DevConcrete::class, $second); } + public function testBindWhenSkipsFalseConditionsAndBindsTheFirstTrueOne(): void + { + $container = new Container; + + $instance = $container->make(BindWhenInterface::class); + + $this->assertInstanceOf(BindWhenTrueConcrete::class, $instance); + } + + public function testBindWhenRespectsTheSingletonAttribute(): void + { + $container = new Container; + + $first = $container->make(BindWhenSingletonInterface::class); + $second = $container->make(BindWhenSingletonInterface::class); + + $this->assertInstanceOf(BindWhenSingletonConcrete::class, $first); + $this->assertSame($first, $second); + } + + public function testBindWhenWithNoPassingConditionDoesNotBind(): void + { + $this->expectException(BindingResolutionException::class); + + $container = new Container; + $container->make(BindWhenNoMatchInterface::class); + } + public function testNoMatchingEnvironmentAndNoWildcardThrowsBindingResolutionException(): void { $this->expectException(BindingResolutionException::class); @@ -1194,6 +1223,47 @@ interface IsSingleton { } +#[BindWhen(BindWhenFalseConcrete::class, static function () { + return false; +})] +#[BindWhen(BindWhenTrueConcrete::class, static function () { + return true; +})] +interface BindWhenInterface +{ +} + +class BindWhenFalseConcrete implements BindWhenInterface +{ +} + +class BindWhenTrueConcrete implements BindWhenInterface +{ +} + +#[BindWhen(BindWhenSingletonConcrete::class, static function () { + return true; +})] +#[Singleton] +interface BindWhenSingletonInterface +{ +} + +class BindWhenSingletonConcrete implements BindWhenSingletonInterface +{ +} + +#[BindWhen(BindWhenNoMatchConcrete::class, static function () { + return false; +})] +interface BindWhenNoMatchInterface +{ +} + +class BindWhenNoMatchConcrete implements BindWhenNoMatchInterface +{ +} + class RequestDto implements SelfBuilding { public function __construct( From 54d17c930cc0102e1612a64db4232bc3cd8097d3 Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 09:54:13 +0100 Subject: [PATCH 02/14] wip --- tests/Container/ContainerTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 37606127bac5..a26d0a220698 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -876,7 +876,7 @@ public function testFlushResetsEnvironmentResolverAndCheckedBindings(): void $this->assertInstanceOf(DevConcrete::class, $second); } - public function testBindWhenSkipsFalseConditionsAndBindsTheFirstTrueOne(): void + public function testBindWhenSkipsFalseConditionsAndBindsFirstTrueCondition(): void { $container = new Container; @@ -885,7 +885,7 @@ public function testBindWhenSkipsFalseConditionsAndBindsTheFirstTrueOne(): void $this->assertInstanceOf(BindWhenTrueConcrete::class, $instance); } - public function testBindWhenRespectsTheSingletonAttribute(): void + public function testBindWhenRespectsSingletonAttribute(): void { $container = new Container; @@ -896,7 +896,7 @@ public function testBindWhenRespectsTheSingletonAttribute(): void $this->assertSame($first, $second); } - public function testBindWhenWithNoPassingConditionDoesNotBind(): void + public function testBindWhenWithNoPassingConditionThrowsBindingResolutionException(): void { $this->expectException(BindingResolutionException::class); From 11a96146f2c424c7960ba373745508fba11b130f Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 10:04:05 +0100 Subject: [PATCH 03/14] wip --- src/Illuminate/Container/Container.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 5c6fa9b31ef3..8aeeab1b3a84 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -1005,9 +1005,6 @@ protected function getConcreteBindingFromAttributes($abstract) return $abstract; } - // BindWhen attributes are evaluated first and, unlike Bind, do not depend on - // the container's environment. The first attribute whose condition returns - // true wins, letting a binding be selected from arbitrary runtime state. $concrete = $this->getConcreteFromBindWhenAttributes($reflected); if ($concrete === null && $this->environmentResolver !== null) { From 615047e8560bdcb93d6c9d9ec5730e719956cab6 Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 10:18:44 +0100 Subject: [PATCH 04/14] wip --- tests/Container/ContainerTest.php | 56 +++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index a26d0a220698..a03def864865 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -876,7 +876,7 @@ public function testFlushResetsEnvironmentResolverAndCheckedBindings(): void $this->assertInstanceOf(DevConcrete::class, $second); } - public function testBindWhenSkipsFalseConditionsAndBindsFirstTrueCondition(): void + public function testBindWhenBindsFirstConditionThatPasses(): void { $container = new Container; @@ -896,7 +896,7 @@ public function testBindWhenRespectsSingletonAttribute(): void $this->assertSame($first, $second); } - public function testBindWhenWithNoPassingConditionThrowsBindingResolutionException(): void + public function testBindWhenThrowsWhenNoConditionPasses(): void { $this->expectException(BindingResolutionException::class); @@ -904,6 +904,26 @@ public function testBindWhenWithNoPassingConditionThrowsBindingResolutionExcepti $container->make(BindWhenNoMatchInterface::class); } + public function testBindWhenTakesPrecedenceOverBind(): void + { + $container = new Container; + $container->resolveEnvironmentUsing(fn () => true); + + $instance = $container->make(BindWhenAndBindInterface::class); + + $this->assertInstanceOf(BindWhenWinsConcrete::class, $instance); + } + + public function testBindWhenFallsThroughToBind(): void + { + $container = new Container; + $container->resolveEnvironmentUsing(fn () => true); + + $instance = $container->make(BindWhenFallbackInterface::class); + + $this->assertInstanceOf(BindFallbackConcrete::class, $instance); + } + public function testNoMatchingEnvironmentAndNoWildcardThrowsBindingResolutionException(): void { $this->expectException(BindingResolutionException::class); @@ -1264,6 +1284,38 @@ class BindWhenNoMatchConcrete implements BindWhenNoMatchInterface { } +#[BindWhen(BindWhenWinsConcrete::class, static function () { + return true; +})] +#[Bind(BindLosesConcrete::class)] +interface BindWhenAndBindInterface +{ +} + +class BindWhenWinsConcrete implements BindWhenAndBindInterface +{ +} + +class BindLosesConcrete implements BindWhenAndBindInterface +{ +} + +#[BindWhen(BindWhenSkippedConcrete::class, static function () { + return false; +})] +#[Bind(BindFallbackConcrete::class)] +interface BindWhenFallbackInterface +{ +} + +class BindWhenSkippedConcrete implements BindWhenFallbackInterface +{ +} + +class BindFallbackConcrete implements BindWhenFallbackInterface +{ +} + class RequestDto implements SelfBuilding { public function __construct( From d5d6cd47a6de5067763873005d43c37b3ebe937b Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 12:11:24 +0100 Subject: [PATCH 05/14] wip --- tests/Container/ContainerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index a03def864865..46ae17d477b8 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -885,7 +885,7 @@ public function testBindWhenBindsFirstConditionThatPasses(): void $this->assertInstanceOf(BindWhenTrueConcrete::class, $instance); } - public function testBindWhenRespectsSingletonAttribute(): void + public function testBindWhenSingletonAttribute(): void { $container = new Container; From 3a8bd09355d758e91b68c782bd2fa99bcfbb707b Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 12:23:52 +0100 Subject: [PATCH 06/14] wip --- tests/Container/ContainerBindWhenTest.php | 143 ++++++++++++++++++++++ tests/Container/ContainerTest.php | 122 ------------------ 2 files changed, 143 insertions(+), 122 deletions(-) create mode 100644 tests/Container/ContainerBindWhenTest.php diff --git a/tests/Container/ContainerBindWhenTest.php b/tests/Container/ContainerBindWhenTest.php new file mode 100644 index 000000000000..1a2077d54711 --- /dev/null +++ b/tests/Container/ContainerBindWhenTest.php @@ -0,0 +1,143 @@ += 8.5.0')] +class ContainerBindWhenTest extends TestCase +{ + protected function tearDown(): void + { + Container::setInstance(null); + + parent::tearDown(); + } + + public function testBindWhenBindsFirstConditionThatPasses(): void + { + $container = new Container; + + $instance = $container->make(BindWhenInterface::class); + + $this->assertInstanceOf(BindWhenTrueConcrete::class, $instance); + } + + public function testBindWhenSingletonAttribute(): void + { + $container = new Container; + + $first = $container->make(BindWhenSingletonInterface::class); + $second = $container->make(BindWhenSingletonInterface::class); + + $this->assertInstanceOf(BindWhenSingletonConcrete::class, $first); + $this->assertSame($first, $second); + } + + public function testBindWhenThrowsWhenNoConditionPasses(): void + { + $this->expectException(BindingResolutionException::class); + + $container = new Container; + $container->make(BindWhenNoMatchInterface::class); + } + + public function testBindWhenTakesPrecedenceOverBind(): void + { + $container = new Container; + $container->resolveEnvironmentUsing(fn () => true); + + $instance = $container->make(BindWhenAndBindInterface::class); + + $this->assertInstanceOf(BindWhenWinsConcrete::class, $instance); + } + + public function testBindWhenFallsThroughToBind(): void + { + $container = new Container; + $container->resolveEnvironmentUsing(fn () => true); + + $instance = $container->make(BindWhenFallbackInterface::class); + + $this->assertInstanceOf(BindFallbackConcrete::class, $instance); + } +} + +#[BindWhen(BindWhenFalseConcrete::class, static function () { + return false; +})] +#[BindWhen(BindWhenTrueConcrete::class, static function () { + return true; +})] +interface BindWhenInterface +{ +} + +class BindWhenFalseConcrete implements BindWhenInterface +{ +} + +class BindWhenTrueConcrete implements BindWhenInterface +{ +} + +#[BindWhen(BindWhenSingletonConcrete::class, static function () { + return true; +})] +#[Singleton] +interface BindWhenSingletonInterface +{ +} + +class BindWhenSingletonConcrete implements BindWhenSingletonInterface +{ +} + +#[BindWhen(BindWhenNoMatchConcrete::class, static function () { + return false; +})] +interface BindWhenNoMatchInterface +{ +} + +class BindWhenNoMatchConcrete implements BindWhenNoMatchInterface +{ +} + +#[BindWhen(BindWhenWinsConcrete::class, static function () { + return true; +})] +#[Bind(BindLosesConcrete::class)] +interface BindWhenAndBindInterface +{ +} + +class BindWhenWinsConcrete implements BindWhenAndBindInterface +{ +} + +class BindLosesConcrete implements BindWhenAndBindInterface +{ +} + +#[BindWhen(BindWhenSkippedConcrete::class, static function () { + return false; +})] +#[Bind(BindFallbackConcrete::class)] +interface BindWhenFallbackInterface +{ +} + +class BindWhenSkippedConcrete implements BindWhenFallbackInterface +{ +} + +class BindFallbackConcrete implements BindWhenFallbackInterface +{ +} diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 46ae17d477b8..36f1cc03a440 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -4,7 +4,6 @@ use Attribute; use Illuminate\Container\Attributes\Bind; -use Illuminate\Container\Attributes\BindWhen; use Illuminate\Container\Attributes\Scoped; use Illuminate\Container\Attributes\Singleton; use Illuminate\Container\Container; @@ -876,54 +875,6 @@ public function testFlushResetsEnvironmentResolverAndCheckedBindings(): void $this->assertInstanceOf(DevConcrete::class, $second); } - public function testBindWhenBindsFirstConditionThatPasses(): void - { - $container = new Container; - - $instance = $container->make(BindWhenInterface::class); - - $this->assertInstanceOf(BindWhenTrueConcrete::class, $instance); - } - - public function testBindWhenSingletonAttribute(): void - { - $container = new Container; - - $first = $container->make(BindWhenSingletonInterface::class); - $second = $container->make(BindWhenSingletonInterface::class); - - $this->assertInstanceOf(BindWhenSingletonConcrete::class, $first); - $this->assertSame($first, $second); - } - - public function testBindWhenThrowsWhenNoConditionPasses(): void - { - $this->expectException(BindingResolutionException::class); - - $container = new Container; - $container->make(BindWhenNoMatchInterface::class); - } - - public function testBindWhenTakesPrecedenceOverBind(): void - { - $container = new Container; - $container->resolveEnvironmentUsing(fn () => true); - - $instance = $container->make(BindWhenAndBindInterface::class); - - $this->assertInstanceOf(BindWhenWinsConcrete::class, $instance); - } - - public function testBindWhenFallsThroughToBind(): void - { - $container = new Container; - $container->resolveEnvironmentUsing(fn () => true); - - $instance = $container->make(BindWhenFallbackInterface::class); - - $this->assertInstanceOf(BindFallbackConcrete::class, $instance); - } - public function testNoMatchingEnvironmentAndNoWildcardThrowsBindingResolutionException(): void { $this->expectException(BindingResolutionException::class); @@ -1243,79 +1194,6 @@ interface IsSingleton { } -#[BindWhen(BindWhenFalseConcrete::class, static function () { - return false; -})] -#[BindWhen(BindWhenTrueConcrete::class, static function () { - return true; -})] -interface BindWhenInterface -{ -} - -class BindWhenFalseConcrete implements BindWhenInterface -{ -} - -class BindWhenTrueConcrete implements BindWhenInterface -{ -} - -#[BindWhen(BindWhenSingletonConcrete::class, static function () { - return true; -})] -#[Singleton] -interface BindWhenSingletonInterface -{ -} - -class BindWhenSingletonConcrete implements BindWhenSingletonInterface -{ -} - -#[BindWhen(BindWhenNoMatchConcrete::class, static function () { - return false; -})] -interface BindWhenNoMatchInterface -{ -} - -class BindWhenNoMatchConcrete implements BindWhenNoMatchInterface -{ -} - -#[BindWhen(BindWhenWinsConcrete::class, static function () { - return true; -})] -#[Bind(BindLosesConcrete::class)] -interface BindWhenAndBindInterface -{ -} - -class BindWhenWinsConcrete implements BindWhenAndBindInterface -{ -} - -class BindLosesConcrete implements BindWhenAndBindInterface -{ -} - -#[BindWhen(BindWhenSkippedConcrete::class, static function () { - return false; -})] -#[Bind(BindFallbackConcrete::class)] -interface BindWhenFallbackInterface -{ -} - -class BindWhenSkippedConcrete implements BindWhenFallbackInterface -{ -} - -class BindFallbackConcrete implements BindWhenFallbackInterface -{ -} - class RequestDto implements SelfBuilding { public function __construct( From 9a36a928ba4571045c48f94eb9fd9d8e33bb535f Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 13:35:27 +0100 Subject: [PATCH 07/14] wip --- tests/Container/ContainerBindWhenFixtures.php | 86 +++++++++++ tests/Container/ContainerBindWhenTest.php | 143 ------------------ tests/Container/ContainerTest.php | 64 ++++++++ 3 files changed, 150 insertions(+), 143 deletions(-) create mode 100644 tests/Container/ContainerBindWhenFixtures.php delete mode 100644 tests/Container/ContainerBindWhenTest.php diff --git a/tests/Container/ContainerBindWhenFixtures.php b/tests/Container/ContainerBindWhenFixtures.php new file mode 100644 index 000000000000..7d4f501aaf78 --- /dev/null +++ b/tests/Container/ContainerBindWhenFixtures.php @@ -0,0 +1,86 @@ += 8.5. They live in a standalone file that is required at runtime + * by the PHP >= 8.5 gated tests so that older PHP versions never compile them. + */ + +#[BindWhen(BindWhenFalseConcrete::class, static function () { + return false; +})] +#[BindWhen(BindWhenTrueConcrete::class, static function () { + return true; +})] +interface BindWhenInterface +{ +} + +class BindWhenFalseConcrete implements BindWhenInterface +{ +} + +class BindWhenTrueConcrete implements BindWhenInterface +{ +} + +#[BindWhen(BindWhenSingletonConcrete::class, static function () { + return true; +})] +#[Singleton] +interface BindWhenSingletonInterface +{ +} + +class BindWhenSingletonConcrete implements BindWhenSingletonInterface +{ +} + +#[BindWhen(BindWhenNoMatchConcrete::class, static function () { + return false; +})] +interface BindWhenNoMatchInterface +{ +} + +class BindWhenNoMatchConcrete implements BindWhenNoMatchInterface +{ +} + +#[BindWhen(BindWhenWinsConcrete::class, static function () { + return true; +})] +#[Bind(BindLosesConcrete::class)] +interface BindWhenAndBindInterface +{ +} + +class BindWhenWinsConcrete implements BindWhenAndBindInterface +{ +} + +class BindLosesConcrete implements BindWhenAndBindInterface +{ +} + +#[BindWhen(BindWhenSkippedConcrete::class, static function () { + return false; +})] +#[Bind(BindFallbackConcrete::class)] +interface BindWhenFallbackInterface +{ +} + +class BindWhenSkippedConcrete implements BindWhenFallbackInterface +{ +} + +class BindFallbackConcrete implements BindWhenFallbackInterface +{ +} diff --git a/tests/Container/ContainerBindWhenTest.php b/tests/Container/ContainerBindWhenTest.php deleted file mode 100644 index 1a2077d54711..000000000000 --- a/tests/Container/ContainerBindWhenTest.php +++ /dev/null @@ -1,143 +0,0 @@ -= 8.5.0')] -class ContainerBindWhenTest extends TestCase -{ - protected function tearDown(): void - { - Container::setInstance(null); - - parent::tearDown(); - } - - public function testBindWhenBindsFirstConditionThatPasses(): void - { - $container = new Container; - - $instance = $container->make(BindWhenInterface::class); - - $this->assertInstanceOf(BindWhenTrueConcrete::class, $instance); - } - - public function testBindWhenSingletonAttribute(): void - { - $container = new Container; - - $first = $container->make(BindWhenSingletonInterface::class); - $second = $container->make(BindWhenSingletonInterface::class); - - $this->assertInstanceOf(BindWhenSingletonConcrete::class, $first); - $this->assertSame($first, $second); - } - - public function testBindWhenThrowsWhenNoConditionPasses(): void - { - $this->expectException(BindingResolutionException::class); - - $container = new Container; - $container->make(BindWhenNoMatchInterface::class); - } - - public function testBindWhenTakesPrecedenceOverBind(): void - { - $container = new Container; - $container->resolveEnvironmentUsing(fn () => true); - - $instance = $container->make(BindWhenAndBindInterface::class); - - $this->assertInstanceOf(BindWhenWinsConcrete::class, $instance); - } - - public function testBindWhenFallsThroughToBind(): void - { - $container = new Container; - $container->resolveEnvironmentUsing(fn () => true); - - $instance = $container->make(BindWhenFallbackInterface::class); - - $this->assertInstanceOf(BindFallbackConcrete::class, $instance); - } -} - -#[BindWhen(BindWhenFalseConcrete::class, static function () { - return false; -})] -#[BindWhen(BindWhenTrueConcrete::class, static function () { - return true; -})] -interface BindWhenInterface -{ -} - -class BindWhenFalseConcrete implements BindWhenInterface -{ -} - -class BindWhenTrueConcrete implements BindWhenInterface -{ -} - -#[BindWhen(BindWhenSingletonConcrete::class, static function () { - return true; -})] -#[Singleton] -interface BindWhenSingletonInterface -{ -} - -class BindWhenSingletonConcrete implements BindWhenSingletonInterface -{ -} - -#[BindWhen(BindWhenNoMatchConcrete::class, static function () { - return false; -})] -interface BindWhenNoMatchInterface -{ -} - -class BindWhenNoMatchConcrete implements BindWhenNoMatchInterface -{ -} - -#[BindWhen(BindWhenWinsConcrete::class, static function () { - return true; -})] -#[Bind(BindLosesConcrete::class)] -interface BindWhenAndBindInterface -{ -} - -class BindWhenWinsConcrete implements BindWhenAndBindInterface -{ -} - -class BindLosesConcrete implements BindWhenAndBindInterface -{ -} - -#[BindWhen(BindWhenSkippedConcrete::class, static function () { - return false; -})] -#[Bind(BindFallbackConcrete::class)] -interface BindWhenFallbackInterface -{ -} - -class BindWhenSkippedConcrete implements BindWhenFallbackInterface -{ -} - -class BindFallbackConcrete implements BindWhenFallbackInterface -{ -} diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 36f1cc03a440..919162f763fc 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -11,6 +11,7 @@ use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\ContextualAttribute; use Illuminate\Contracts\Container\SelfBuilding; +use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerExceptionInterface; use stdClass; @@ -18,6 +19,16 @@ class ContainerTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + + // Fixtures use static closures in PHP attributes, which only work in PHP 8.5+. + if (PHP_VERSION_ID >= 80500) { + require_once __DIR__.'/ContainerBindWhenFixtures.php'; + } + } + protected function tearDown(): void { Container::setInstance(null); @@ -875,6 +886,59 @@ public function testFlushResetsEnvironmentResolverAndCheckedBindings(): void $this->assertInstanceOf(DevConcrete::class, $second); } + #[RequiresPhp('>= 8.5.0')] + public function testBindWhenBindsFirstConditionThatPasses(): void + { + $container = new Container; + + $instance = $container->make(BindWhenInterface::class); + + $this->assertInstanceOf(BindWhenTrueConcrete::class, $instance); + } + + #[RequiresPhp('>= 8.5.0')] + public function testBindWhenSingletonAttribute(): void + { + $container = new Container; + + $first = $container->make(BindWhenSingletonInterface::class); + $second = $container->make(BindWhenSingletonInterface::class); + + $this->assertInstanceOf(BindWhenSingletonConcrete::class, $first); + $this->assertSame($first, $second); + } + + #[RequiresPhp('>= 8.5.0')] + public function testBindWhenThrowsWhenNoConditionPasses(): void + { + $this->expectException(BindingResolutionException::class); + + $container = new Container; + $container->make(BindWhenNoMatchInterface::class); + } + + #[RequiresPhp('>= 8.5.0')] + public function testBindWhenTakesPrecedenceOverBind(): void + { + $container = new Container; + $container->resolveEnvironmentUsing(fn () => true); + + $instance = $container->make(BindWhenAndBindInterface::class); + + $this->assertInstanceOf(BindWhenWinsConcrete::class, $instance); + } + + #[RequiresPhp('>= 8.5.0')] + public function testBindWhenFallsThroughToBind(): void + { + $container = new Container; + $container->resolveEnvironmentUsing(fn () => true); + + $instance = $container->make(BindWhenFallbackInterface::class); + + $this->assertInstanceOf(BindFallbackConcrete::class, $instance); + } + public function testNoMatchingEnvironmentAndNoWildcardThrowsBindingResolutionException(): void { $this->expectException(BindingResolutionException::class); From 6f6dc23b7e04fb71fd7e017e47d73e53ae46f75d Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 13:37:15 +0100 Subject: [PATCH 08/14] wip --- tests/Container/ContainerTest.php | 2 +- tests/Container/{ => Fixtures}/ContainerBindWhenFixtures.php | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/Container/{ => Fixtures}/ContainerBindWhenFixtures.php (100%) diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 919162f763fc..eef3ca30cf5d 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -25,7 +25,7 @@ protected function setUp(): void // Fixtures use static closures in PHP attributes, which only work in PHP 8.5+. if (PHP_VERSION_ID >= 80500) { - require_once __DIR__.'/ContainerBindWhenFixtures.php'; + require_once __DIR__.'/Fixtures/ContainerBindWhenFixtures.php'; } } diff --git a/tests/Container/ContainerBindWhenFixtures.php b/tests/Container/Fixtures/ContainerBindWhenFixtures.php similarity index 100% rename from tests/Container/ContainerBindWhenFixtures.php rename to tests/Container/Fixtures/ContainerBindWhenFixtures.php From eda16b1739e6706cffc08322d03b49df393338dd Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 13:49:39 +0100 Subject: [PATCH 09/14] wip --- tests/Container/Fixtures/ContainerBindWhenFixtures.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/Container/Fixtures/ContainerBindWhenFixtures.php b/tests/Container/Fixtures/ContainerBindWhenFixtures.php index 7d4f501aaf78..e1131ad74065 100644 --- a/tests/Container/Fixtures/ContainerBindWhenFixtures.php +++ b/tests/Container/Fixtures/ContainerBindWhenFixtures.php @@ -6,12 +6,6 @@ use Illuminate\Container\Attributes\BindWhen; use Illuminate\Container\Attributes\Singleton; -/* - * These fixtures embed static closures inside attribute arguments, which is only - * valid on PHP >= 8.5. They live in a standalone file that is required at runtime - * by the PHP >= 8.5 gated tests so that older PHP versions never compile them. - */ - #[BindWhen(BindWhenFalseConcrete::class, static function () { return false; })] From 5d7ffb6cad04d9d8dbc598bac9634a22e51c25d1 Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 14:05:28 +0100 Subject: [PATCH 10/14] use attribute order prescedence --- src/Illuminate/Container/Container.php | 72 ++++++++----------- tests/Container/ContainerTest.php | 11 +++ .../Fixtures/ContainerBindWhenFixtures.php | 16 +++++ 3 files changed, 56 insertions(+), 43 deletions(-) diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 8aeeab1b3a84..7d1db886a655 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -1005,11 +1005,7 @@ protected function getConcreteBindingFromAttributes($abstract) return $abstract; } - $concrete = $this->getConcreteFromBindWhenAttributes($reflected); - - if ($concrete === null && $this->environmentResolver !== null) { - $concrete = $this->getConcreteFromBindAttributes($reflected); - } + $concrete = $this->resolveConcreteFromAttributes($reflected); if ($concrete === null) { return $abstract; @@ -1025,61 +1021,51 @@ protected function getConcreteBindingFromAttributes($abstract) } /** - * Resolve the concrete for the first BindWhen attribute whose condition passes. + * Resolve the concrete from the Bind and BindWhen attributes in declaration order. * * @param ReflectionClass $reflected * @return class-string|null */ - protected function getConcreteFromBindWhenAttributes(ReflectionClass $reflected) + protected function resolveConcreteFromAttributes(ReflectionClass $reflected) { - foreach ($reflected->getAttributes(BindWhen::class) as $reflectedAttribute) { - $instance = $reflectedAttribute->newInstance(); - - if (($instance->condition)($this)) { - return $instance->concrete; - } - } + $wildcard = null; - return null; - } + foreach ($reflected->getAttributes() as $reflectedAttribute) { + $name = $reflectedAttribute->getName(); - /** - * Resolve the concrete from the Bind attributes for the current environment. - * - * @param ReflectionClass $reflected - * @return class-string|null - */ - protected function getConcreteFromBindAttributes(ReflectionClass $reflected) - { - $bindAttributes = $reflected->getAttributes(Bind::class); + if ($name === BindWhen::class) { + $instance = $reflectedAttribute->newInstance(); - if ($bindAttributes === []) { - return null; - } + if (($instance->condition)($this)) { + return $instance->concrete; + } - $concrete = $maybeConcrete = null; + continue; + } - foreach ($bindAttributes as $reflectedAttribute) { - $instance = $reflectedAttribute->newInstance(); + if ($name === Bind::class) { + // Bind attributes only apply when an environment resolver is set. + if ($this->environmentResolver === null) { + continue; + } - if ($instance->environments === ['*']) { - $maybeConcrete = $instance->concrete; + $instance = $reflectedAttribute->newInstance(); - continue; - } + if ($instance->environments === ['*']) { + // Remember the first wildcard as a fallback, but keep looking for a + // more specific match (either an environment or a later BindWhen). + $wildcard ??= $instance->concrete; - if ($this->currentEnvironmentIs($instance->environments)) { - $concrete = $instance->concrete; + continue; + } - break; + if ($this->currentEnvironmentIs($instance->environments)) { + return $instance->concrete; + } } } - if ($maybeConcrete !== null && $concrete === null) { - $concrete = $maybeConcrete; - } - - return $concrete; + return $wildcard; } /** diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index eef3ca30cf5d..de79c39cbc8b 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -939,6 +939,17 @@ public function testBindWhenFallsThroughToBind(): void $this->assertInstanceOf(BindFallbackConcrete::class, $instance); } + #[RequiresPhp('>= 8.5.0')] + public function testBindAndBindWhenResolveInDeclarationOrder(): void + { + $container = new Container; + $container->resolveEnvironmentUsing(fn ($environments) => in_array('prod', (array) $environments)); + + $instance = $container->make(BindBeforeBindWhenInterface::class); + + $this->assertInstanceOf(BindBeforeConcrete::class, $instance); + } + public function testNoMatchingEnvironmentAndNoWildcardThrowsBindingResolutionException(): void { $this->expectException(BindingResolutionException::class); diff --git a/tests/Container/Fixtures/ContainerBindWhenFixtures.php b/tests/Container/Fixtures/ContainerBindWhenFixtures.php index e1131ad74065..81bfb96fc398 100644 --- a/tests/Container/Fixtures/ContainerBindWhenFixtures.php +++ b/tests/Container/Fixtures/ContainerBindWhenFixtures.php @@ -78,3 +78,19 @@ class BindWhenSkippedConcrete implements BindWhenFallbackInterface class BindFallbackConcrete implements BindWhenFallbackInterface { } + +#[Bind(BindBeforeConcrete::class, environments: 'prod')] +#[BindWhen(BindWhenAfterConcrete::class, static function () { + return true; +})] +interface BindBeforeBindWhenInterface +{ +} + +class BindBeforeConcrete implements BindBeforeBindWhenInterface +{ +} + +class BindWhenAfterConcrete implements BindBeforeBindWhenInterface +{ +} From b4939ee3fea07e33fa006a2335cd461e59d53290 Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 14:27:35 +0100 Subject: [PATCH 11/14] wip --- src/Illuminate/Container/Container.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 7d1db886a655..49e5b1115f58 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -1043,17 +1043,10 @@ protected function resolveConcreteFromAttributes(ReflectionClass $reflected) continue; } - if ($name === Bind::class) { - // Bind attributes only apply when an environment resolver is set. - if ($this->environmentResolver === null) { - continue; - } - + if ($name === Bind::class && $this->environmentResolver !== null) { $instance = $reflectedAttribute->newInstance(); if ($instance->environments === ['*']) { - // Remember the first wildcard as a fallback, but keep looking for a - // more specific match (either an environment or a later BindWhen). $wildcard ??= $instance->concrete; continue; From a0e14141bd6aa4b78b90f84298a739c3dc9b6504 Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 14:49:35 +0100 Subject: [PATCH 12/14] wip --- tests/Container/ContainerTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index de79c39cbc8b..34b65f445586 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -23,7 +23,6 @@ protected function setUp(): void { parent::setUp(); - // Fixtures use static closures in PHP attributes, which only work in PHP 8.5+. if (PHP_VERSION_ID >= 80500) { require_once __DIR__.'/Fixtures/ContainerBindWhenFixtures.php'; } From 17fb761b46cd052388445c7e9623f8d3bf4372c0 Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 14:54:30 +0100 Subject: [PATCH 13/14] wip --- tests/Container/ContainerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 34b65f445586..da0fc6c6deab 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -23,7 +23,7 @@ protected function setUp(): void { parent::setUp(); - if (PHP_VERSION_ID >= 80500) { + if (version_compare(PHP_VERSION, '8.5.0', '>=')) { require_once __DIR__.'/Fixtures/ContainerBindWhenFixtures.php'; } } From 9db6355ca32384bd643951cfbefb784e46f4ed3e Mon Sep 17 00:00:00 2001 From: Jamie York Date: Wed, 22 Jul 2026 14:57:59 +0100 Subject: [PATCH 14/14] wip --- tests/Container/ContainerTest.php | 2 +- tests/Container/Fixtures/ContainerBindWhenFixtures.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index da0fc6c6deab..f1130c5b5274 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -942,7 +942,7 @@ public function testBindWhenFallsThroughToBind(): void public function testBindAndBindWhenResolveInDeclarationOrder(): void { $container = new Container; - $container->resolveEnvironmentUsing(fn ($environments) => in_array('prod', (array) $environments)); + $container->resolveEnvironmentUsing(fn ($environments) => in_array('foobar', (array) $environments)); $instance = $container->make(BindBeforeBindWhenInterface::class); diff --git a/tests/Container/Fixtures/ContainerBindWhenFixtures.php b/tests/Container/Fixtures/ContainerBindWhenFixtures.php index 81bfb96fc398..d29eb50e0e2a 100644 --- a/tests/Container/Fixtures/ContainerBindWhenFixtures.php +++ b/tests/Container/Fixtures/ContainerBindWhenFixtures.php @@ -79,7 +79,7 @@ class BindFallbackConcrete implements BindWhenFallbackInterface { } -#[Bind(BindBeforeConcrete::class, environments: 'prod')] +#[Bind(BindBeforeConcrete::class, environments: 'foobar')] #[BindWhen(BindWhenAfterConcrete::class, static function () { return true; })]