Skip to content

Commit

Permalink
[11.x] Support attributes in app()->call() (#52428)
Browse files Browse the repository at this point in the history
* feat: support attributes in `app()->call()`

* feat: support attributes in route dependencies resolution

* style: apply fixes from style-ci
  • Loading branch information
innocenzi authored Aug 17, 2024
1 parent 273b41a commit 14ee5d4
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 23 deletions.
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;
}
}

0 comments on commit 14ee5d4

Please sign in to comment.