Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/Illuminate/Container/Attributes/BindWhen.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Illuminate\Container\Attributes;

use Attribute;
use Closure;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class BindWhen
{
/**
* The concrete class to bind to.
*
* @var class-string
*/
public string $concrete;

/**
* The condition that determines if the binding should apply.
*
* @var \Closure(\Illuminate\Contracts\Container\Container): bool
*/
public Closure $condition;

/**
* Create a new attribute instance.
*
* @param class-string $concrete
* @param \Closure(\Illuminate\Contracts\Container\Container): bool $condition
*/
public function __construct(string $concrete, Closure $condition)
{
$this->concrete = $concrete;
$this->condition = $condition;
}
}
69 changes: 42 additions & 27 deletions src/Illuminate/Container/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -981,16 +982,15 @@ 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;
}

return $this->getConcreteBindingFromAttributes($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
Expand All @@ -1005,45 +1005,60 @@ protected function getConcreteBindingFromAttributes($abstract)
return $abstract;
}

$bindAttributes = $reflected->getAttributes(Bind::class);
$concrete = $this->resolveConcreteFromAttributes($reflected);

if ($bindAttributes === []) {
if ($concrete === null) {
return $abstract;
}

$concrete = $maybeConcrete = null;
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 from the Bind and BindWhen attributes in declaration order.
*
* @param ReflectionClass<object> $reflected
* @return class-string|null
*/
protected function resolveConcreteFromAttributes(ReflectionClass $reflected)
{
$wildcard = null;

foreach ($reflected->getAttributes() as $reflectedAttribute) {
$name = $reflectedAttribute->getName();

foreach ($bindAttributes as $reflectedAttribute) {
$instance = $reflectedAttribute->newInstance();
if ($name === BindWhen::class) {
$instance = $reflectedAttribute->newInstance();

if ($instance->environments === ['*']) {
$maybeConcrete = $instance->concrete;
if (($instance->condition)($this)) {
return $instance->concrete;
}

continue;
}

if ($this->currentEnvironmentIs($instance->environments)) {
$concrete = $instance->concrete;
if ($name === Bind::class && $this->environmentResolver !== null) {
$instance = $reflectedAttribute->newInstance();

break;
}
}
if ($instance->environments === ['*']) {
$wildcard ??= $instance->concrete;

if ($maybeConcrete !== null && $concrete === null) {
$concrete = $maybeConcrete;
}
continue;
}

if ($concrete === null) {
return $abstract;
if ($this->currentEnvironmentIs($instance->environments)) {
return $instance->concrete;
}
}
}

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 $wildcard;
}

/**
Expand Down
74 changes: 74 additions & 0 deletions tests/Container/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,23 @@
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;
use TypeError;

class ContainerTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

if (version_compare(PHP_VERSION, '8.5.0', '>=')) {
require_once __DIR__.'/Fixtures/ContainerBindWhenFixtures.php';
}
}

protected function tearDown(): void
{
Container::setInstance(null);
Expand Down Expand Up @@ -875,6 +885,70 @@ 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);
}

#[RequiresPhp('>= 8.5.0')]
public function testBindAndBindWhenResolveInDeclarationOrder(): void
{
$container = new Container;
$container->resolveEnvironmentUsing(fn ($environments) => in_array('foobar', (array) $environments));

$instance = $container->make(BindBeforeBindWhenInterface::class);

$this->assertInstanceOf(BindBeforeConcrete::class, $instance);
}

public function testNoMatchingEnvironmentAndNoWildcardThrowsBindingResolutionException(): void
{
$this->expectException(BindingResolutionException::class);
Expand Down
96 changes: 96 additions & 0 deletions tests/Container/Fixtures/ContainerBindWhenFixtures.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace Illuminate\Tests\Container;

use Illuminate\Container\Attributes\Bind;
use Illuminate\Container\Attributes\BindWhen;
use Illuminate\Container\Attributes\Singleton;

#[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
{
}

#[Bind(BindBeforeConcrete::class, environments: 'foobar')]
#[BindWhen(BindWhenAfterConcrete::class, static function () {
return true;
})]
interface BindBeforeBindWhenInterface
{
}

class BindBeforeConcrete implements BindBeforeBindWhenInterface
{
}

class BindWhenAfterConcrete implements BindBeforeBindWhenInterface
{
}
Loading