Skip to content

Commit

Permalink
[11.x] Add support for acting on attributes through container (#51934)
Browse files Browse the repository at this point in the history
* feat: support contextual attribute binding

* feat: add after resolving attribute callback

* test: update first class resolution test

* style: apply changes from style-ci

* test: fix second assertion

* formatting

* fix(contextual-attributes): support primitives

* feat(contextual-binding): support `resolve` method in contextual attributes

* style: apply fixes from style-ci

* formatting

* formatting

* add after support

* add config attribute by default

---------

Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
innocenzi and taylorotwell authored Jul 1, 2024
1 parent 0de031f commit cb64466
Show file tree
Hide file tree
Showing 5 changed files with 477 additions and 3 deletions.
30 changes: 30 additions & 0 deletions src/Illuminate/Container/Attributes/Config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Illuminate\Container\Attributes;

use Attribute;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Container\ContextualAttribute;

#[Attribute(Attribute::TARGET_PARAMETER)]
class Config implements ContextualAttribute
{
/**
* Create a new class instance.
*/
public function __construct(public string $key, public mixed $default = null)
{
}

/**
* Resolve the configuration value.
*
* @param self $attribute
* @param \Illuminate\Contracts\Container\Container $container
* @return mixed
*/
public static function resolve(self $attribute, Container $container)
{
return $container->make('config')->get($attribute->key, $attribute->default);
}
}
124 changes: 121 additions & 3 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\ContextualAttribute;
use LogicException;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
Expand Down Expand Up @@ -108,6 +110,13 @@ class Container implements ArrayAccess, ContainerContract
*/
public $contextual = [];

/**
* The contextual attribute handlers.
*
* @var array[]
*/
public $contextualAttributes = [];

/**
* All of the registered rebound callbacks.
*
Expand Down Expand Up @@ -157,6 +166,13 @@ class Container implements ArrayAccess, ContainerContract
*/
protected $afterResolvingCallbacks = [];

/**
* All of the after resolving attribute callbacks by class type.
*
* @var array[]
*/
protected $afterResolvingAttributeCallbacks = [];

/**
* Define a contextual binding.
*
Expand All @@ -174,6 +190,18 @@ public function when($concrete)
return new ContextualBindingBuilder($this, $aliases);
}

/**
* Define a contextual binding based on an attribute.
*
* @param string $attribute
* @param \Closure $handler
* @return void
*/
public function whenHasAttribute(string $attribute, Closure $handler)
{
$this->contextualAttributes[$attribute] = $handler;
}

/**
* Determine if the given abstract type has been bound.
*
Expand Down Expand Up @@ -923,7 +951,11 @@ public function build($concrete)
if (is_null($constructor)) {
array_pop($this->buildStack);

return new $concrete;
$this->fireAfterResolvingAttributeCallbacks(
$reflector->getAttributes(), $instance = new $concrete
);

return $instance;
}

$dependencies = $constructor->getParameters();
Expand All @@ -941,7 +973,11 @@ public function build($concrete)

array_pop($this->buildStack);

return $reflector->newInstanceArgs($instances);
$this->fireAfterResolvingAttributeCallbacks(
$reflector->getAttributes(), $instance = $reflector->newInstanceArgs($instances)
);

return $instance;
}

/**
Expand All @@ -966,13 +1002,21 @@ protected function resolveDependencies(array $dependencies)
continue;
}

$result = null;

if (! is_null($attribute = $this->getContextualAttributeFromDependency($dependency))) {
$result = $this->resolveFromAttribute($attribute);
}

// 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))
$result ??= is_null(Util::getParameterClassName($dependency))
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);

$this->fireAfterResolvingAttributeCallbacks($dependency->getAttributes(), $result);

if ($dependency->isVariadic()) {
$results = array_merge($results, $result);
} else {
Expand Down Expand Up @@ -1017,6 +1061,17 @@ protected function getLastParameterOverride()
return count($this->with) ? end($this->with) : [];
}

/**
* Get a contextual attribute from a dependency.
*
* @param ReflectionParameter $dependency
* @return \ReflectionAttribute|null
*/
protected function getContextualAttributeFromDependency($dependency)
{
return $dependency->getAttributes(ContextualAttribute::class, ReflectionAttribute::IS_INSTANCEOF)[0] ?? null;
}

/**
* Resolve a non-class hinted primitive dependency.
*
Expand Down Expand Up @@ -1097,6 +1152,29 @@ protected function resolveVariadicClass(ReflectionParameter $parameter)
return array_map(fn ($abstract) => $this->resolve($abstract), $concrete);
}

/**
* Resolve a dependency based on an attribute.
*
* @param \ReflectionAttribute $attribute
* @return mixed
*/
protected function resolveFromAttribute(ReflectionAttribute $attribute)
{
$handler = $this->contextualAttributes[$attribute->getName()] ?? null;

$instance = $attribute->newInstance();

if (is_null($handler) && method_exists($instance, 'resolve')) {
$handler = $instance->resolve(...);
}

if (is_null($handler)) {
throw new BindingResolutionException("Contextual binding attribute [{$attribute->getName()}] has no registered handler.");
}

return $handler($instance, $this);
}

/**
* Throw an exception that the concrete is not instantiable.
*
Expand Down Expand Up @@ -1193,6 +1271,18 @@ public function afterResolving($abstract, ?Closure $callback = null)
}
}

/**
* Register a new after resolving attribute callback for all types.
*
* @param string $attribute
* @param \Closure $callback
* @return void
*/
public function afterResolvingAttribute(string $attribute, \Closure $callback)
{
$this->afterResolvingAttributeCallbacks[$attribute][] = $callback;
}

/**
* Fire all of the before resolving callbacks.
*
Expand Down Expand Up @@ -1260,6 +1350,34 @@ protected function fireAfterResolvingCallbacks($abstract, $object)
);
}

/**
* Fire all of the after resolving attribute callbacks.
*
* @param \ReflectionAttribute[] $abstract
* @param mixed $object
* @return void
*/
protected function fireAfterResolvingAttributeCallbacks(array $attributes, $object)
{
foreach ($attributes as $attribute) {
if (is_a($attribute->getName(), ContextualAttribute::class, true)) {
$instance = $attribute->newInstance();

if (method_exists($instance, 'after')) {
$instance->after($instance, $object, $this);
}
}

$callbacks = $this->getCallbacksForType(
$attribute->getName(), $object, $this->afterResolvingAttributeCallbacks
);

foreach ($callbacks as $callback) {
$callback($attribute->newInstance(), $object, $this);
}
}
}

/**
* Get all callbacks for a given type.
*
Expand Down
8 changes: 8 additions & 0 deletions src/Illuminate/Contracts/Container/ContextualAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Illuminate\Contracts\Container;

interface ContextualAttribute
{
//
}
137 changes: 137 additions & 0 deletions tests/Container/AfterResolvingAttributeCallbackTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

namespace Illuminate\Tests\Container;

use Attribute;
use Illuminate\Container\Container;
use PHPUnit\Framework\TestCase;

class AfterResolvingAttributeCallbackTest extends TestCase
{
public function testCallbackIsCalledAfterDependencyResolutionWithAttribute()
{
$container = new Container();

$container->afterResolvingAttribute(ContainerTestOnTenant::class, function (ContainerTestOnTenant $attribute, HasTenantImpl $hasTenantImpl, Container $container) {
$hasTenantImpl->onTenant($attribute->tenant);
});

$hasTenantA = $container->make(ContainerTestHasTenantImplPropertyWithTenantA::class);
$this->assertInstanceOf(HasTenantImpl::class, $hasTenantA->property);
$this->assertEquals(Tenant::TenantA, $hasTenantA->property->tenant);

$hasTenantB = $container->make(ContainerTestHasTenantImplPropertyWithTenantB::class);
$this->assertInstanceOf(HasTenantImpl::class, $hasTenantB->property);
$this->assertEquals(Tenant::TenantB, $hasTenantB->property->tenant);
}

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

$container->afterResolvingAttribute(
ContainerTestBootable::class,
fn ($_, $instance, Container $container) => method_exists($instance, 'booting') && $container->call([$instance, 'booting'])
);

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

$this->assertInstanceOf(ContainerTestHasBootable::class, $instance);
$this->assertTrue($instance->hasBooted);
}

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

$container->afterResolvingAttribute(ContainerTestConfiguresClass::class, function (ContainerTestConfiguresClass $attribute, $class) {
$class->value = $attribute->value;
});

$container->when(ContainerTestHasSelfConfiguringAttributeAndConstructor::class)
->needs('$value')
->give('no-the-right-value');

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

$this->assertInstanceOf(ContainerTestHasSelfConfiguringAttributeAndConstructor::class, $instance);
$this->assertEquals('the-right-value', $instance->value);
}
}

#[Attribute(Attribute::TARGET_PARAMETER)]
final class ContainerTestOnTenant
{
public function __construct(
public readonly Tenant $tenant
) {
}
}

enum Tenant
{
case TenantA;
case TenantB;
}

final class HasTenantImpl
{
public ?Tenant $tenant = null;

public function onTenant(Tenant $tenant): void
{
$this->tenant = $tenant;
}
}

final class ContainerTestHasTenantImplPropertyWithTenantA
{
public function __construct(
#[ContainerTestOnTenant(Tenant::TenantA)]
public readonly HasTenantImpl $property
) {
}
}

final class ContainerTestHasTenantImplPropertyWithTenantB
{
public function __construct(
#[ContainerTestOnTenant(Tenant::TenantB)]
public readonly HasTenantImpl $property
) {
}
}

#[Attribute(Attribute::TARGET_CLASS)]
final class ContainerTestConfiguresClass
{
public function __construct(
public readonly string $value
) {
}
}

#[ContainerTestConfiguresClass(value: 'the-right-value')]
final class ContainerTestHasSelfConfiguringAttributeAndConstructor
{
public function __construct(
public string $value
) {
}
}

#[Attribute(Attribute::TARGET_CLASS)]
final class ContainerTestBootable
{
}

#[ContainerTestBootable]
final class ContainerTestHasBootable
{
public bool $hasBooted = false;

public function booting(): void
{
$this->hasBooted = true;
}
}
Loading

0 comments on commit cb64466

Please sign in to comment.