Skip to content

Commit 14ee5d4

Browse files
authored
[11.x] Support attributes in app()->call() (#52428)
* feat: support attributes in `app()->call()` * feat: support attributes in route dependencies resolution * style: apply fixes from style-ci
1 parent 273b41a commit 14ee5d4

7 files changed

+159
-23
lines changed

src/Illuminate/Container/BoundMethod.php

+24-9
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ protected static function callClass($container, $target, array $parameters = [],
6363
}
6464

6565
return static::call(
66-
$container, [$container->make($segments[0]), $method], $parameters
66+
$container,
67+
[$container->make($segments[0]), $method],
68+
$parameters
6769
);
6870
}
6971

@@ -159,34 +161,47 @@ protected static function getCallReflector($callback)
159161
*
160162
* @throws \Illuminate\Contracts\Container\BindingResolutionException
161163
*/
162-
protected static function addDependencyForCallParameter($container, $parameter,
163-
array &$parameters, &$dependencies)
164-
{
164+
protected static function addDependencyForCallParameter(
165+
$container,
166+
$parameter,
167+
array &$parameters,
168+
&$dependencies
169+
) {
170+
$pendingDependencies = [];
171+
165172
if (array_key_exists($paramName = $parameter->getName(), $parameters)) {
166-
$dependencies[] = $parameters[$paramName];
173+
$pendingDependencies[] = $parameters[$paramName];
167174

168175
unset($parameters[$paramName]);
176+
} elseif ($attribute = Util::getContextualAttributeFromDependency($parameter)) {
177+
$pendingDependencies[] = $container->resolveFromAttribute($attribute);
169178
} elseif (! is_null($className = Util::getParameterClassName($parameter))) {
170179
if (array_key_exists($className, $parameters)) {
171-
$dependencies[] = $parameters[$className];
180+
$pendingDependencies[] = $parameters[$className];
172181

173182
unset($parameters[$className]);
174183
} elseif ($parameter->isVariadic()) {
175184
$variadicDependencies = $container->make($className);
176185

177-
$dependencies = array_merge($dependencies, is_array($variadicDependencies)
186+
$pendingDependencies = array_merge($pendingDependencies, is_array($variadicDependencies)
178187
? $variadicDependencies
179188
: [$variadicDependencies]);
180189
} else {
181-
$dependencies[] = $container->make($className);
190+
$pendingDependencies[] = $container->make($className);
182191
}
183192
} elseif ($parameter->isDefaultValueAvailable()) {
184-
$dependencies[] = $parameter->getDefaultValue();
193+
$pendingDependencies[] = $parameter->getDefaultValue();
185194
} elseif (! $parameter->isOptional() && ! array_key_exists($paramName, $parameters)) {
186195
$message = "Unable to resolve dependency [{$parameter}] in class {$parameter->getDeclaringClass()->getName()}";
187196

188197
throw new BindingResolutionException($message);
189198
}
199+
200+
foreach ($pendingDependencies as $dependency) {
201+
$container->fireAfterResolvingAttributeCallbacks($parameter->getAttributes(), $dependency);
202+
}
203+
204+
$dependencies = array_merge($dependencies, $pendingDependencies);
190205
}
191206

192207
/**

src/Illuminate/Container/Container.php

+3-14
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,7 @@ protected function resolveDependencies(array $dependencies)
10101010

10111011
$result = null;
10121012

1013-
if (! is_null($attribute = $this->getContextualAttributeFromDependency($dependency))) {
1013+
if (! is_null($attribute = Util::getContextualAttributeFromDependency($dependency))) {
10141014
$result = $this->resolveFromAttribute($attribute);
10151015
}
10161016

@@ -1067,17 +1067,6 @@ protected function getLastParameterOverride()
10671067
return count($this->with) ? end($this->with) : [];
10681068
}
10691069

1070-
/**
1071-
* Get a contextual attribute from a dependency.
1072-
*
1073-
* @param ReflectionParameter $dependency
1074-
* @return \ReflectionAttribute|null
1075-
*/
1076-
protected function getContextualAttributeFromDependency($dependency)
1077-
{
1078-
return $dependency->getAttributes(ContextualAttribute::class, ReflectionAttribute::IS_INSTANCEOF)[0] ?? null;
1079-
}
1080-
10811070
/**
10821071
* Resolve a non-class hinted primitive dependency.
10831072
*
@@ -1164,7 +1153,7 @@ protected function resolveVariadicClass(ReflectionParameter $parameter)
11641153
* @param \ReflectionAttribute $attribute
11651154
* @return mixed
11661155
*/
1167-
protected function resolveFromAttribute(ReflectionAttribute $attribute)
1156+
public function resolveFromAttribute(ReflectionAttribute $attribute)
11681157
{
11691158
$handler = $this->contextualAttributes[$attribute->getName()] ?? null;
11701159

@@ -1363,7 +1352,7 @@ protected function fireAfterResolvingCallbacks($abstract, $object)
13631352
* @param mixed $object
13641353
* @return void
13651354
*/
1366-
protected function fireAfterResolvingAttributeCallbacks(array $attributes, $object)
1355+
public function fireAfterResolvingAttributeCallbacks(array $attributes, $object)
13671356
{
13681357
foreach ($attributes as $attribute) {
13691358
if (is_a($attribute->getName(), ContextualAttribute::class, true)) {

src/Illuminate/Container/Util.php

+13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace Illuminate\Container;
44

55
use Closure;
6+
use Illuminate\Contracts\Container\ContextualAttribute;
7+
use ReflectionAttribute;
68
use ReflectionNamedType;
79

810
/**
@@ -71,4 +73,15 @@ public static function getParameterClassName($parameter)
7173

7274
return $name;
7375
}
76+
77+
/**
78+
* Get a contextual attribute from a dependency.
79+
*
80+
* @param ReflectionParameter $dependency
81+
* @return \ReflectionAttribute|null
82+
*/
83+
public static function getContextualAttributeFromDependency($dependency)
84+
{
85+
return $dependency->getAttributes(ContextualAttribute::class, ReflectionAttribute::IS_INSTANCEOF)[0] ?? null;
86+
}
7487
}

src/Illuminate/Routing/ResolvesRouteDependencies.php

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Illuminate\Routing;
44

5+
use Illuminate\Container\Util;
56
use Illuminate\Support\Arr;
67
use Illuminate\Support\Reflector;
78
use ReflectionClass;
@@ -57,6 +58,8 @@ public function resolveMethodDependencies(array $parameters, ReflectionFunctionA
5758
$parameter->isDefaultValueAvailable()) {
5859
$this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue());
5960
}
61+
62+
$this->container->fireAfterResolvingAttributeCallbacks($parameter->getAttributes(), $instance);
6063
}
6164

6265
return $parameters;
@@ -74,6 +77,10 @@ protected function transformDependency(ReflectionParameter $parameter, $paramete
7477
{
7578
$className = Reflector::getParameterClassName($parameter);
7679

80+
if ($attribute = Util::getContextualAttributeFromDependency($parameter)) {
81+
return $this->container->resolveFromAttribute($attribute);
82+
}
83+
7784
// If the parameter has a type-hinted class, we will check to see if it is already in
7885
// the list of parameters. If it is we will just skip it as it is probably a model
7986
// binding and we do not want to mess with those; otherwise, we resolve it here.

tests/Container/AfterResolvingAttributeCallbackTest.php

+15
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@ public function testCallbackIsCalledAfterClassWithConstructorAndAttributeIsResol
5757
$this->assertInstanceOf(ContainerTestHasSelfConfiguringAttributeAndConstructor::class, $instance);
5858
$this->assertEquals('the-right-value', $instance->value);
5959
}
60+
61+
public function testCallbackIsCalledOnAppCall()
62+
{
63+
$container = new Container();
64+
65+
$container->afterResolvingAttribute(ContainerTestOnTenant::class, function (ContainerTestOnTenant $attribute, HasTenantImpl $hasTenantImpl, Container $container) {
66+
$hasTenantImpl->onTenant($attribute->tenant);
67+
});
68+
69+
$tenant = $container->call(function (#[ContainerTestOnTenant(Tenant::TenantA)] HasTenantImpl $property) {
70+
return $property->tenant;
71+
});
72+
73+
$this->assertEquals(Tenant::TenantA, $tenant);
74+
}
6075
}
6176

6277
#[Attribute(Attribute::TARGET_PARAMETER)]

tests/Container/ContextualAttributeBindingTest.php

+27
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,33 @@ public function testStorageAttribute()
208208

209209
$container->make(StorageTest::class);
210210
}
211+
212+
public function testInjectionWithAttributeOnAppCall()
213+
{
214+
$container = new Container;
215+
216+
$person = $container->call(function (ContainerTestHasConfigValueWithResolvePropertyAndAfterCallback $hasAttribute) {
217+
return $hasAttribute->person;
218+
});
219+
220+
$this->assertEquals('Taylor', $person->name);
221+
}
222+
223+
public function testAttributeOnAppCall()
224+
{
225+
$container = new Container;
226+
$container->singleton('config', fn () => new Repository([
227+
'app' => [
228+
'timezone' => 'Europe/Paris',
229+
],
230+
]));
231+
232+
$value = $container->call(function (#[Config('app.timezone')] string $value) {
233+
return $value;
234+
});
235+
236+
$this->assertEquals('Europe/Paris', $value);
237+
}
211238
}
212239

213240
#[Attribute(Attribute::TARGET_PARAMETER)]

tests/Routing/RoutingRouteTest.php

+70
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
namespace Illuminate\Tests\Routing;
44

5+
use Attribute;
56
use Closure;
67
use DateTime;
78
use Exception;
89
use Illuminate\Auth\Middleware\Authenticate;
910
use Illuminate\Auth\Middleware\Authorize;
11+
use Illuminate\Config\Repository;
12+
use Illuminate\Container\Attributes\Config;
1013
use Illuminate\Container\Container;
1114
use Illuminate\Contracts\Routing\Registrar;
1215
use Illuminate\Contracts\Support\Responsable;
@@ -1107,6 +1110,48 @@ public function testModelBindingThroughIOC()
11071110
$this->assertSame('TAYLOR', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent());
11081111
}
11091112

1113+
public function testRouteDependenciesCanBeResolvedThroughAttributes()
1114+
{
1115+
$container = new Container;
1116+
$container->singleton('config', fn () => new Repository([
1117+
'app' => [
1118+
'timezone' => 'Europe/Paris',
1119+
],
1120+
]));
1121+
$router = new Router(new Dispatcher, $container);
1122+
$container->instance(Registrar::class, $router);
1123+
$container->bind(CallableDispatcherContract::class, fn ($app) => new CallableDispatcher($app));
1124+
$router->get('foo', [
1125+
'middleware' => SubstituteBindings::class,
1126+
'uses' => function (#[Config('app.timezone')] string $value) {
1127+
return $value;
1128+
},
1129+
]);
1130+
1131+
$this->assertSame('Europe/Paris', $router->dispatch(Request::create('foo', 'GET'))->getContent());
1132+
}
1133+
1134+
public function testAfterResolvingAttributeCallbackIsCalledOnRouteDependenciesResolution()
1135+
{
1136+
$container = new Container();
1137+
$router = new Router(new Dispatcher, $container);
1138+
$container->instance(Registrar::class, $router);
1139+
$container->bind(CallableDispatcherContract::class, fn ($app) => new CallableDispatcher($app));
1140+
1141+
$container->afterResolvingAttribute(RoutingTestOnTenant::class, function (RoutingTestOnTenant $attribute, RoutingTestHasTenantImpl $hasTenantImpl, Container $container) {
1142+
$hasTenantImpl->onTenant($attribute->tenant);
1143+
});
1144+
1145+
$router->get('foo', [
1146+
'middleware' => SubstituteBindings::class,
1147+
'uses' => function (#[RoutingTestOnTenant(RoutingTestTenant::TenantA)] RoutingTestHasTenantImpl $property) {
1148+
return $property->tenant->name;
1149+
},
1150+
]);
1151+
1152+
$this->assertSame('TenantA', $router->dispatch(Request::create('foo', 'GET'))->getContent());
1153+
}
1154+
11101155
public function testGroupMerging()
11111156
{
11121157
$old = ['prefix' => 'foo/bar/'];
@@ -2639,3 +2684,28 @@ public function handle($request, Closure $next)
26392684
return $next($request);
26402685
}
26412686
}
2687+
2688+
#[Attribute(Attribute::TARGET_PARAMETER)]
2689+
final class RoutingTestOnTenant
2690+
{
2691+
public function __construct(
2692+
public readonly RoutingTestTenant $tenant
2693+
) {
2694+
}
2695+
}
2696+
2697+
enum RoutingTestTenant
2698+
{
2699+
case TenantA;
2700+
case TenantB;
2701+
}
2702+
2703+
final class RoutingTestHasTenantImpl
2704+
{
2705+
public ?RoutingTestTenant $tenant = null;
2706+
2707+
public function onTenant(RoutingTestTenant $tenant): void
2708+
{
2709+
$this->tenant = $tenant;
2710+
}
2711+
}

0 commit comments

Comments
 (0)