Skip to content

[10.x] Add dependency resolver attributes #49640

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
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
55 changes: 55 additions & 0 deletions src/Illuminate/Config/Attributes/InjectFromConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Illuminate\Config\Attributes;

use Attribute;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Container\DependencyResolver;

#[Attribute(Attribute::TARGET_PARAMETER)]
class InjectFromConfig implements DependencyResolver
{
/**
* Key to get from config.
*
* @var string|array
*/
private $key;

/**
* Default in case config is not present or bound.
*
* @var mixed
*/
private $default;

/**
* Create a new InjectFromConfig instance.
*
* @param array|string $key
* @param mixed $default
* @return void
*/
public function __construct($key, $default = null)
{
$this->key = $key;
$this->default = $default;
}

/**
* Resolve the dependency from the container.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function resolve(Container $container): mixed
{
if (! $container->bound('config')) {
return value($this->default);
}

return $container->make('config')->get($this->key, $this->default);
}
}
27 changes: 21 additions & 6 deletions src/Illuminate/Container/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Container\CircularDependencyException;
use Illuminate\Contracts\Container\Container as ContainerContract;
use Illuminate\Contracts\Container\DependencyResolver;
use LogicException;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
Expand Down Expand Up @@ -974,12 +976,25 @@ protected function resolveDependencies(array $dependencies)
continue;
}

// If the class is null, it means the dependency is a string or some other
// primitive type which we can not resolve since it is not a class and
// we will just bomb out with an error since we have no-where to go.
$result = is_null(Util::getParameterClassName($dependency))
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);
$resolvers = $dependency->getAttributes(
DependencyResolver::class,
ReflectionAttribute::IS_INSTANCEOF,
);

if (count($resolvers) > 0) {
if (count($resolvers) !== 1) {
throw new LogicException('Parameter cannot have multiple dependency resolvers');
}

$result = array_values($resolvers)[0]->newInstance()->resolve($this);
} else {
// If the class is null, it means the dependency is a string or some other
// primitive type which we can not resolve since it is not a class and
// we will just bomb out with an error since we have no-where to go.
$result = is_null(Util::getParameterClassName($dependency))
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);
}

if ($dependency->isVariadic()) {
$results = array_merge($results, $result);
Expand Down
14 changes: 14 additions & 0 deletions src/Illuminate/Contracts/Container/DependencyResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Illuminate\Contracts\Container;

interface DependencyResolver
{
/**
* Resolve the dependency from the container.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return mixed
*/
public function resolve(Container $container): mixed;
}
36 changes: 36 additions & 0 deletions tests/Config/Attributes/InjectFromConfigTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Illuminate\Tests\Config\Attributes;

use Illuminate\Config\Attributes\InjectFromConfig;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Container\Container;
use Mockery as m;
use PHPUnit\Framework\TestCase;

class InjectFromConfigTest extends TestCase
{
public function testItReturnsDefaultWhenConfigNotBound()
{
$attribute = new InjectFromConfig('app.name', 'default');

$container = m::mock(Container::class);
$container->shouldReceive('bound')->with('config')->once()->andReturnFalse();

$this->assertSame('default', $attribute->resolve($container));
}

public function testItGetsFromConfig()
{
$attribute = new InjectFromConfig('app.name', 'default');

$config = m::mock(Repository::class);
$config->shouldReceive('get')->with('app.name', 'default')->once()->andReturn('Laravel');

$container = m::mock(Container::class);
$container->shouldReceive('bound')->with('config')->once()->andReturnTrue();
$container->shouldReceive('make')->with('config')->once()->andReturn($config);

$this->assertSame('Laravel', $attribute->resolve($container));
}
}
39 changes: 39 additions & 0 deletions tests/Container/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace Illuminate\Tests\Container;

use Attribute;
use Illuminate\Container\Container;
use Illuminate\Container\EntryNotFoundException;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Container\Container as ContainerContract;
use Illuminate\Contracts\Container\DependencyResolver;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
use stdClass;
Expand Down Expand Up @@ -150,6 +153,24 @@ public function testAutoConcreteResolution()
$this->assertInstanceOf(ContainerConcreteStub::class, $container->make(ContainerConcreteStub::class));
}

public function testDependencyResolverAttributes()
{
$container = new Container();
$result = $container->make(ContainerDependencyResolver::class);
$this->assertSame('bar', $result->foo);
}

public function testParametersTakePrecedentOverDependencyResolverAttributes()
{
$container = new Container();

$result = $container->make(ContainerDependencyResolver::class, [
'foo' => 'baz',
]);

$this->assertSame('baz', $result->foo);
}

public function testSharedConcreteResolution()
{
$container = new Container;
Expand Down Expand Up @@ -718,6 +739,24 @@ class ContainerConcreteStub
//
}

class ContainerDependencyResolver
{
public function __construct(
#[AlwaysResolveAsBar]
public string $foo,
) {
}
}

#[Attribute(Attribute::TARGET_PARAMETER)]
class AlwaysResolveAsBar implements DependencyResolver
{
public function resolve(ContainerContract $container): mixed
{
return 'bar';
}
}

interface IContainerContractStub
{
//
Expand Down