Skip to content

Commit cb64466

Browse files
[11.x] Add support for acting on attributes through container (#51934)
* 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]>
1 parent 0de031f commit cb64466

File tree

5 files changed

+477
-3
lines changed

5 files changed

+477
-3
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Illuminate\Container\Attributes;
4+
5+
use Attribute;
6+
use Illuminate\Contracts\Container\Container;
7+
use Illuminate\Contracts\Container\ContextualAttribute;
8+
9+
#[Attribute(Attribute::TARGET_PARAMETER)]
10+
class Config implements ContextualAttribute
11+
{
12+
/**
13+
* Create a new class instance.
14+
*/
15+
public function __construct(public string $key, public mixed $default = null)
16+
{
17+
}
18+
19+
/**
20+
* Resolve the configuration value.
21+
*
22+
* @param self $attribute
23+
* @param \Illuminate\Contracts\Container\Container $container
24+
* @return mixed
25+
*/
26+
public static function resolve(self $attribute, Container $container)
27+
{
28+
return $container->make('config')->get($attribute->key, $attribute->default);
29+
}
30+
}

src/Illuminate/Container/Container.php

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
use Illuminate\Contracts\Container\BindingResolutionException;
99
use Illuminate\Contracts\Container\CircularDependencyException;
1010
use Illuminate\Contracts\Container\Container as ContainerContract;
11+
use Illuminate\Contracts\Container\ContextualAttribute;
1112
use LogicException;
13+
use ReflectionAttribute;
1214
use ReflectionClass;
1315
use ReflectionException;
1416
use ReflectionFunction;
@@ -108,6 +110,13 @@ class Container implements ArrayAccess, ContainerContract
108110
*/
109111
public $contextual = [];
110112

113+
/**
114+
* The contextual attribute handlers.
115+
*
116+
* @var array[]
117+
*/
118+
public $contextualAttributes = [];
119+
111120
/**
112121
* All of the registered rebound callbacks.
113122
*
@@ -157,6 +166,13 @@ class Container implements ArrayAccess, ContainerContract
157166
*/
158167
protected $afterResolvingCallbacks = [];
159168

169+
/**
170+
* All of the after resolving attribute callbacks by class type.
171+
*
172+
* @var array[]
173+
*/
174+
protected $afterResolvingAttributeCallbacks = [];
175+
160176
/**
161177
* Define a contextual binding.
162178
*
@@ -174,6 +190,18 @@ public function when($concrete)
174190
return new ContextualBindingBuilder($this, $aliases);
175191
}
176192

193+
/**
194+
* Define a contextual binding based on an attribute.
195+
*
196+
* @param string $attribute
197+
* @param \Closure $handler
198+
* @return void
199+
*/
200+
public function whenHasAttribute(string $attribute, Closure $handler)
201+
{
202+
$this->contextualAttributes[$attribute] = $handler;
203+
}
204+
177205
/**
178206
* Determine if the given abstract type has been bound.
179207
*
@@ -923,7 +951,11 @@ public function build($concrete)
923951
if (is_null($constructor)) {
924952
array_pop($this->buildStack);
925953

926-
return new $concrete;
954+
$this->fireAfterResolvingAttributeCallbacks(
955+
$reflector->getAttributes(), $instance = new $concrete
956+
);
957+
958+
return $instance;
927959
}
928960

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

942974
array_pop($this->buildStack);
943975

944-
return $reflector->newInstanceArgs($instances);
976+
$this->fireAfterResolvingAttributeCallbacks(
977+
$reflector->getAttributes(), $instance = $reflector->newInstanceArgs($instances)
978+
);
979+
980+
return $instance;
945981
}
946982

947983
/**
@@ -966,13 +1002,21 @@ protected function resolveDependencies(array $dependencies)
9661002
continue;
9671003
}
9681004

1005+
$result = null;
1006+
1007+
if (! is_null($attribute = $this->getContextualAttributeFromDependency($dependency))) {
1008+
$result = $this->resolveFromAttribute($attribute);
1009+
}
1010+
9691011
// If the class is null, it means the dependency is a string or some other
9701012
// primitive type which we can not resolve since it is not a class and
9711013
// we will just bomb out with an error since we have no-where to go.
972-
$result = is_null(Util::getParameterClassName($dependency))
1014+
$result ??= is_null(Util::getParameterClassName($dependency))
9731015
? $this->resolvePrimitive($dependency)
9741016
: $this->resolveClass($dependency);
9751017

1018+
$this->fireAfterResolvingAttributeCallbacks($dependency->getAttributes(), $result);
1019+
9761020
if ($dependency->isVariadic()) {
9771021
$results = array_merge($results, $result);
9781022
} else {
@@ -1017,6 +1061,17 @@ protected function getLastParameterOverride()
10171061
return count($this->with) ? end($this->with) : [];
10181062
}
10191063

1064+
/**
1065+
* Get a contextual attribute from a dependency.
1066+
*
1067+
* @param ReflectionParameter $dependency
1068+
* @return \ReflectionAttribute|null
1069+
*/
1070+
protected function getContextualAttributeFromDependency($dependency)
1071+
{
1072+
return $dependency->getAttributes(ContextualAttribute::class, ReflectionAttribute::IS_INSTANCEOF)[0] ?? null;
1073+
}
1074+
10201075
/**
10211076
* Resolve a non-class hinted primitive dependency.
10221077
*
@@ -1097,6 +1152,29 @@ protected function resolveVariadicClass(ReflectionParameter $parameter)
10971152
return array_map(fn ($abstract) => $this->resolve($abstract), $concrete);
10981153
}
10991154

1155+
/**
1156+
* Resolve a dependency based on an attribute.
1157+
*
1158+
* @param \ReflectionAttribute $attribute
1159+
* @return mixed
1160+
*/
1161+
protected function resolveFromAttribute(ReflectionAttribute $attribute)
1162+
{
1163+
$handler = $this->contextualAttributes[$attribute->getName()] ?? null;
1164+
1165+
$instance = $attribute->newInstance();
1166+
1167+
if (is_null($handler) && method_exists($instance, 'resolve')) {
1168+
$handler = $instance->resolve(...);
1169+
}
1170+
1171+
if (is_null($handler)) {
1172+
throw new BindingResolutionException("Contextual binding attribute [{$attribute->getName()}] has no registered handler.");
1173+
}
1174+
1175+
return $handler($instance, $this);
1176+
}
1177+
11001178
/**
11011179
* Throw an exception that the concrete is not instantiable.
11021180
*
@@ -1193,6 +1271,18 @@ public function afterResolving($abstract, ?Closure $callback = null)
11931271
}
11941272
}
11951273

1274+
/**
1275+
* Register a new after resolving attribute callback for all types.
1276+
*
1277+
* @param string $attribute
1278+
* @param \Closure $callback
1279+
* @return void
1280+
*/
1281+
public function afterResolvingAttribute(string $attribute, \Closure $callback)
1282+
{
1283+
$this->afterResolvingAttributeCallbacks[$attribute][] = $callback;
1284+
}
1285+
11961286
/**
11971287
* Fire all of the before resolving callbacks.
11981288
*
@@ -1260,6 +1350,34 @@ protected function fireAfterResolvingCallbacks($abstract, $object)
12601350
);
12611351
}
12621352

1353+
/**
1354+
* Fire all of the after resolving attribute callbacks.
1355+
*
1356+
* @param \ReflectionAttribute[] $abstract
1357+
* @param mixed $object
1358+
* @return void
1359+
*/
1360+
protected function fireAfterResolvingAttributeCallbacks(array $attributes, $object)
1361+
{
1362+
foreach ($attributes as $attribute) {
1363+
if (is_a($attribute->getName(), ContextualAttribute::class, true)) {
1364+
$instance = $attribute->newInstance();
1365+
1366+
if (method_exists($instance, 'after')) {
1367+
$instance->after($instance, $object, $this);
1368+
}
1369+
}
1370+
1371+
$callbacks = $this->getCallbacksForType(
1372+
$attribute->getName(), $object, $this->afterResolvingAttributeCallbacks
1373+
);
1374+
1375+
foreach ($callbacks as $callback) {
1376+
$callback($attribute->newInstance(), $object, $this);
1377+
}
1378+
}
1379+
}
1380+
12631381
/**
12641382
* Get all callbacks for a given type.
12651383
*
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Illuminate\Contracts\Container;
4+
5+
interface ContextualAttribute
6+
{
7+
//
8+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Container;
4+
5+
use Attribute;
6+
use Illuminate\Container\Container;
7+
use PHPUnit\Framework\TestCase;
8+
9+
class AfterResolvingAttributeCallbackTest extends TestCase
10+
{
11+
public function testCallbackIsCalledAfterDependencyResolutionWithAttribute()
12+
{
13+
$container = new Container();
14+
15+
$container->afterResolvingAttribute(ContainerTestOnTenant::class, function (ContainerTestOnTenant $attribute, HasTenantImpl $hasTenantImpl, Container $container) {
16+
$hasTenantImpl->onTenant($attribute->tenant);
17+
});
18+
19+
$hasTenantA = $container->make(ContainerTestHasTenantImplPropertyWithTenantA::class);
20+
$this->assertInstanceOf(HasTenantImpl::class, $hasTenantA->property);
21+
$this->assertEquals(Tenant::TenantA, $hasTenantA->property->tenant);
22+
23+
$hasTenantB = $container->make(ContainerTestHasTenantImplPropertyWithTenantB::class);
24+
$this->assertInstanceOf(HasTenantImpl::class, $hasTenantB->property);
25+
$this->assertEquals(Tenant::TenantB, $hasTenantB->property->tenant);
26+
}
27+
28+
public function testCallbackIsCalledAfterClassWithAttributeIsResolved()
29+
{
30+
$container = new Container();
31+
32+
$container->afterResolvingAttribute(
33+
ContainerTestBootable::class,
34+
fn ($_, $instance, Container $container) => method_exists($instance, 'booting') && $container->call([$instance, 'booting'])
35+
);
36+
37+
$instance = $container->make(ContainerTestHasBootable::class);
38+
39+
$this->assertInstanceOf(ContainerTestHasBootable::class, $instance);
40+
$this->assertTrue($instance->hasBooted);
41+
}
42+
43+
public function testCallbackIsCalledAfterClassWithConstructorAndAttributeIsResolved()
44+
{
45+
$container = new Container();
46+
47+
$container->afterResolvingAttribute(ContainerTestConfiguresClass::class, function (ContainerTestConfiguresClass $attribute, $class) {
48+
$class->value = $attribute->value;
49+
});
50+
51+
$container->when(ContainerTestHasSelfConfiguringAttributeAndConstructor::class)
52+
->needs('$value')
53+
->give('no-the-right-value');
54+
55+
$instance = $container->make(ContainerTestHasSelfConfiguringAttributeAndConstructor::class);
56+
57+
$this->assertInstanceOf(ContainerTestHasSelfConfiguringAttributeAndConstructor::class, $instance);
58+
$this->assertEquals('the-right-value', $instance->value);
59+
}
60+
}
61+
62+
#[Attribute(Attribute::TARGET_PARAMETER)]
63+
final class ContainerTestOnTenant
64+
{
65+
public function __construct(
66+
public readonly Tenant $tenant
67+
) {
68+
}
69+
}
70+
71+
enum Tenant
72+
{
73+
case TenantA;
74+
case TenantB;
75+
}
76+
77+
final class HasTenantImpl
78+
{
79+
public ?Tenant $tenant = null;
80+
81+
public function onTenant(Tenant $tenant): void
82+
{
83+
$this->tenant = $tenant;
84+
}
85+
}
86+
87+
final class ContainerTestHasTenantImplPropertyWithTenantA
88+
{
89+
public function __construct(
90+
#[ContainerTestOnTenant(Tenant::TenantA)]
91+
public readonly HasTenantImpl $property
92+
) {
93+
}
94+
}
95+
96+
final class ContainerTestHasTenantImplPropertyWithTenantB
97+
{
98+
public function __construct(
99+
#[ContainerTestOnTenant(Tenant::TenantB)]
100+
public readonly HasTenantImpl $property
101+
) {
102+
}
103+
}
104+
105+
#[Attribute(Attribute::TARGET_CLASS)]
106+
final class ContainerTestConfiguresClass
107+
{
108+
public function __construct(
109+
public readonly string $value
110+
) {
111+
}
112+
}
113+
114+
#[ContainerTestConfiguresClass(value: 'the-right-value')]
115+
final class ContainerTestHasSelfConfiguringAttributeAndConstructor
116+
{
117+
public function __construct(
118+
public string $value
119+
) {
120+
}
121+
}
122+
123+
#[Attribute(Attribute::TARGET_CLASS)]
124+
final class ContainerTestBootable
125+
{
126+
}
127+
128+
#[ContainerTestBootable]
129+
final class ContainerTestHasBootable
130+
{
131+
public bool $hasBooted = false;
132+
133+
public function booting(): void
134+
{
135+
$this->hasBooted = true;
136+
}
137+
}

0 commit comments

Comments
 (0)