diff --git a/src/Illuminate/Container/BoundMethod.php b/src/Illuminate/Container/BoundMethod.php index 0bac7faef108..d719e918217d 100644 --- a/src/Illuminate/Container/BoundMethod.php +++ b/src/Illuminate/Container/BoundMethod.php @@ -63,7 +63,9 @@ protected static function callClass($container, $target, array $parameters = [], } return static::call( - $container, [$container->make($segments[0]), $method], $parameters + $container, + [$container->make($segments[0]), $method], + $parameters ); } @@ -159,34 +161,47 @@ protected static function getCallReflector($callback) * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ - protected static function addDependencyForCallParameter($container, $parameter, - array &$parameters, &$dependencies) - { + protected static function addDependencyForCallParameter( + $container, + $parameter, + array &$parameters, + &$dependencies + ) { + $pendingDependencies = []; + if (array_key_exists($paramName = $parameter->getName(), $parameters)) { - $dependencies[] = $parameters[$paramName]; + $pendingDependencies[] = $parameters[$paramName]; unset($parameters[$paramName]); + } elseif ($attribute = Util::getContextualAttributeFromDependency($parameter)) { + $pendingDependencies[] = $container->resolveFromAttribute($attribute); } elseif (! is_null($className = Util::getParameterClassName($parameter))) { if (array_key_exists($className, $parameters)) { - $dependencies[] = $parameters[$className]; + $pendingDependencies[] = $parameters[$className]; unset($parameters[$className]); } elseif ($parameter->isVariadic()) { $variadicDependencies = $container->make($className); - $dependencies = array_merge($dependencies, is_array($variadicDependencies) + $pendingDependencies = array_merge($pendingDependencies, is_array($variadicDependencies) ? $variadicDependencies : [$variadicDependencies]); } else { - $dependencies[] = $container->make($className); + $pendingDependencies[] = $container->make($className); } } elseif ($parameter->isDefaultValueAvailable()) { - $dependencies[] = $parameter->getDefaultValue(); + $pendingDependencies[] = $parameter->getDefaultValue(); } elseif (! $parameter->isOptional() && ! array_key_exists($paramName, $parameters)) { $message = "Unable to resolve dependency [{$parameter}] in class {$parameter->getDeclaringClass()->getName()}"; throw new BindingResolutionException($message); } + + foreach ($pendingDependencies as $dependency) { + $container->fireAfterResolvingAttributeCallbacks($parameter->getAttributes(), $dependency); + } + + $dependencies = array_merge($dependencies, $pendingDependencies); } /** diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 787ecaf69657..9837bc47f991 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -1010,7 +1010,7 @@ protected function resolveDependencies(array $dependencies) $result = null; - if (! is_null($attribute = $this->getContextualAttributeFromDependency($dependency))) { + if (! is_null($attribute = Util::getContextualAttributeFromDependency($dependency))) { $result = $this->resolveFromAttribute($attribute); } @@ -1067,17 +1067,6 @@ 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. * @@ -1164,7 +1153,7 @@ protected function resolveVariadicClass(ReflectionParameter $parameter) * @param \ReflectionAttribute $attribute * @return mixed */ - protected function resolveFromAttribute(ReflectionAttribute $attribute) + public function resolveFromAttribute(ReflectionAttribute $attribute) { $handler = $this->contextualAttributes[$attribute->getName()] ?? null; @@ -1363,7 +1352,7 @@ protected function fireAfterResolvingCallbacks($abstract, $object) * @param mixed $object * @return void */ - protected function fireAfterResolvingAttributeCallbacks(array $attributes, $object) + public function fireAfterResolvingAttributeCallbacks(array $attributes, $object) { foreach ($attributes as $attribute) { if (is_a($attribute->getName(), ContextualAttribute::class, true)) { diff --git a/src/Illuminate/Container/Util.php b/src/Illuminate/Container/Util.php index 8d5023b4a514..ae1ff41fa1cc 100644 --- a/src/Illuminate/Container/Util.php +++ b/src/Illuminate/Container/Util.php @@ -3,6 +3,8 @@ namespace Illuminate\Container; use Closure; +use Illuminate\Contracts\Container\ContextualAttribute; +use ReflectionAttribute; use ReflectionNamedType; /** @@ -71,4 +73,15 @@ public static function getParameterClassName($parameter) return $name; } + + /** + * Get a contextual attribute from a dependency. + * + * @param ReflectionParameter $dependency + * @return \ReflectionAttribute|null + */ + public static function getContextualAttributeFromDependency($dependency) + { + return $dependency->getAttributes(ContextualAttribute::class, ReflectionAttribute::IS_INSTANCEOF)[0] ?? null; + } } diff --git a/src/Illuminate/Routing/ResolvesRouteDependencies.php b/src/Illuminate/Routing/ResolvesRouteDependencies.php index f5fc2fcb2a31..9bd32a89a965 100644 --- a/src/Illuminate/Routing/ResolvesRouteDependencies.php +++ b/src/Illuminate/Routing/ResolvesRouteDependencies.php @@ -2,6 +2,7 @@ namespace Illuminate\Routing; +use Illuminate\Container\Util; use Illuminate\Support\Arr; use Illuminate\Support\Reflector; use ReflectionClass; @@ -57,6 +58,8 @@ public function resolveMethodDependencies(array $parameters, ReflectionFunctionA $parameter->isDefaultValueAvailable()) { $this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue()); } + + $this->container->fireAfterResolvingAttributeCallbacks($parameter->getAttributes(), $instance); } return $parameters; @@ -74,6 +77,10 @@ protected function transformDependency(ReflectionParameter $parameter, $paramete { $className = Reflector::getParameterClassName($parameter); + if ($attribute = Util::getContextualAttributeFromDependency($parameter)) { + return $this->container->resolveFromAttribute($attribute); + } + // If the parameter has a type-hinted class, we will check to see if it is already in // the list of parameters. If it is we will just skip it as it is probably a model // binding and we do not want to mess with those; otherwise, we resolve it here. diff --git a/tests/Container/AfterResolvingAttributeCallbackTest.php b/tests/Container/AfterResolvingAttributeCallbackTest.php index 545db091af3b..ea3aa4744413 100644 --- a/tests/Container/AfterResolvingAttributeCallbackTest.php +++ b/tests/Container/AfterResolvingAttributeCallbackTest.php @@ -57,6 +57,21 @@ public function testCallbackIsCalledAfterClassWithConstructorAndAttributeIsResol $this->assertInstanceOf(ContainerTestHasSelfConfiguringAttributeAndConstructor::class, $instance); $this->assertEquals('the-right-value', $instance->value); } + + public function testCallbackIsCalledOnAppCall() + { + $container = new Container(); + + $container->afterResolvingAttribute(ContainerTestOnTenant::class, function (ContainerTestOnTenant $attribute, HasTenantImpl $hasTenantImpl, Container $container) { + $hasTenantImpl->onTenant($attribute->tenant); + }); + + $tenant = $container->call(function (#[ContainerTestOnTenant(Tenant::TenantA)] HasTenantImpl $property) { + return $property->tenant; + }); + + $this->assertEquals(Tenant::TenantA, $tenant); + } } #[Attribute(Attribute::TARGET_PARAMETER)] diff --git a/tests/Container/ContextualAttributeBindingTest.php b/tests/Container/ContextualAttributeBindingTest.php index 88995d20bb3f..4b99e20fe51e 100644 --- a/tests/Container/ContextualAttributeBindingTest.php +++ b/tests/Container/ContextualAttributeBindingTest.php @@ -208,6 +208,33 @@ public function testStorageAttribute() $container->make(StorageTest::class); } + + public function testInjectionWithAttributeOnAppCall() + { + $container = new Container; + + $person = $container->call(function (ContainerTestHasConfigValueWithResolvePropertyAndAfterCallback $hasAttribute) { + return $hasAttribute->person; + }); + + $this->assertEquals('Taylor', $person->name); + } + + public function testAttributeOnAppCall() + { + $container = new Container; + $container->singleton('config', fn () => new Repository([ + 'app' => [ + 'timezone' => 'Europe/Paris', + ], + ])); + + $value = $container->call(function (#[Config('app.timezone')] string $value) { + return $value; + }); + + $this->assertEquals('Europe/Paris', $value); + } } #[Attribute(Attribute::TARGET_PARAMETER)] diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index 519b36b112a4..bb7964a7b07b 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -2,11 +2,14 @@ namespace Illuminate\Tests\Routing; +use Attribute; use Closure; use DateTime; use Exception; use Illuminate\Auth\Middleware\Authenticate; use Illuminate\Auth\Middleware\Authorize; +use Illuminate\Config\Repository; +use Illuminate\Container\Attributes\Config; use Illuminate\Container\Container; use Illuminate\Contracts\Routing\Registrar; use Illuminate\Contracts\Support\Responsable; @@ -1107,6 +1110,48 @@ public function testModelBindingThroughIOC() $this->assertSame('TAYLOR', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent()); } + public function testRouteDependenciesCanBeResolvedThroughAttributes() + { + $container = new Container; + $container->singleton('config', fn () => new Repository([ + 'app' => [ + 'timezone' => 'Europe/Paris', + ], + ])); + $router = new Router(new Dispatcher, $container); + $container->instance(Registrar::class, $router); + $container->bind(CallableDispatcherContract::class, fn ($app) => new CallableDispatcher($app)); + $router->get('foo', [ + 'middleware' => SubstituteBindings::class, + 'uses' => function (#[Config('app.timezone')] string $value) { + return $value; + }, + ]); + + $this->assertSame('Europe/Paris', $router->dispatch(Request::create('foo', 'GET'))->getContent()); + } + + public function testAfterResolvingAttributeCallbackIsCalledOnRouteDependenciesResolution() + { + $container = new Container(); + $router = new Router(new Dispatcher, $container); + $container->instance(Registrar::class, $router); + $container->bind(CallableDispatcherContract::class, fn ($app) => new CallableDispatcher($app)); + + $container->afterResolvingAttribute(RoutingTestOnTenant::class, function (RoutingTestOnTenant $attribute, RoutingTestHasTenantImpl $hasTenantImpl, Container $container) { + $hasTenantImpl->onTenant($attribute->tenant); + }); + + $router->get('foo', [ + 'middleware' => SubstituteBindings::class, + 'uses' => function (#[RoutingTestOnTenant(RoutingTestTenant::TenantA)] RoutingTestHasTenantImpl $property) { + return $property->tenant->name; + }, + ]); + + $this->assertSame('TenantA', $router->dispatch(Request::create('foo', 'GET'))->getContent()); + } + public function testGroupMerging() { $old = ['prefix' => 'foo/bar/']; @@ -2639,3 +2684,28 @@ public function handle($request, Closure $next) return $next($request); } } + +#[Attribute(Attribute::TARGET_PARAMETER)] +final class RoutingTestOnTenant +{ + public function __construct( + public readonly RoutingTestTenant $tenant + ) { + } +} + +enum RoutingTestTenant +{ + case TenantA; + case TenantB; +} + +final class RoutingTestHasTenantImpl +{ + public ?RoutingTestTenant $tenant = null; + + public function onTenant(RoutingTestTenant $tenant): void + { + $this->tenant = $tenant; + } +}