Skip to content
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

[11.x] Support attributes in app()->call() #52428

Merged
merged 3 commits into from
Aug 17, 2024
Merged
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
33 changes: 24 additions & 9 deletions src/Illuminate/Container/BoundMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down Expand Up @@ -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);
}

/**
Expand Down
17 changes: 3 additions & 14 deletions src/Illuminate/Container/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)) {
Expand Down
13 changes: 13 additions & 0 deletions src/Illuminate/Container/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Illuminate\Container;

use Closure;
use Illuminate\Contracts\Container\ContextualAttribute;
use ReflectionAttribute;
use ReflectionNamedType;

/**
Expand Down Expand Up @@ -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;
}
}
7 changes: 7 additions & 0 deletions src/Illuminate/Routing/ResolvesRouteDependencies.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Illuminate\Routing;

use Illuminate\Container\Util;
use Illuminate\Support\Arr;
use Illuminate\Support\Reflector;
use ReflectionClass;
Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions tests/Container/AfterResolvingAttributeCallbackTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
27 changes: 27 additions & 0 deletions tests/Container/ContextualAttributeBindingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
70 changes: 70 additions & 0 deletions tests/Routing/RoutingRouteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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/'];
Expand Down Expand Up @@ -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;
}
}