diff --git a/src/Illuminate/Container/Attributes/Config.php b/src/Illuminate/Container/Attributes/Config.php new file mode 100644 index 000000000000..0133708a39d4 --- /dev/null +++ b/src/Illuminate/Container/Attributes/Config.php @@ -0,0 +1,30 @@ +make('config')->get($attribute->key, $attribute->default); + } +} diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index b854c5162e53..1fdaadee322f 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -8,7 +8,9 @@ use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\CircularDependencyException; use Illuminate\Contracts\Container\Container as ContainerContract; +use Illuminate\Contracts\Container\ContextualAttribute; use LogicException; +use ReflectionAttribute; use ReflectionClass; use ReflectionException; use ReflectionFunction; @@ -108,6 +110,13 @@ class Container implements ArrayAccess, ContainerContract */ public $contextual = []; + /** + * The contextual attribute handlers. + * + * @var array[] + */ + public $contextualAttributes = []; + /** * All of the registered rebound callbacks. * @@ -157,6 +166,13 @@ class Container implements ArrayAccess, ContainerContract */ protected $afterResolvingCallbacks = []; + /** + * All of the after resolving attribute callbacks by class type. + * + * @var array[] + */ + protected $afterResolvingAttributeCallbacks = []; + /** * Define a contextual binding. * @@ -174,6 +190,18 @@ public function when($concrete) return new ContextualBindingBuilder($this, $aliases); } + /** + * Define a contextual binding based on an attribute. + * + * @param string $attribute + * @param \Closure $handler + * @return void + */ + public function whenHasAttribute(string $attribute, Closure $handler) + { + $this->contextualAttributes[$attribute] = $handler; + } + /** * Determine if the given abstract type has been bound. * @@ -923,7 +951,11 @@ public function build($concrete) if (is_null($constructor)) { array_pop($this->buildStack); - return new $concrete; + $this->fireAfterResolvingAttributeCallbacks( + $reflector->getAttributes(), $instance = new $concrete + ); + + return $instance; } $dependencies = $constructor->getParameters(); @@ -941,7 +973,11 @@ public function build($concrete) array_pop($this->buildStack); - return $reflector->newInstanceArgs($instances); + $this->fireAfterResolvingAttributeCallbacks( + $reflector->getAttributes(), $instance = $reflector->newInstanceArgs($instances) + ); + + return $instance; } /** @@ -966,13 +1002,21 @@ protected function resolveDependencies(array $dependencies) continue; } + $result = null; + + if (! is_null($attribute = $this->getContextualAttributeFromDependency($dependency))) { + $result = $this->resolveFromAttribute($attribute); + } + // If the class is null, it means the dependency is a string or some other // primitive type which we can not resolve since it is not a class and // we will just bomb out with an error since we have no-where to go. - $result = is_null(Util::getParameterClassName($dependency)) + $result ??= is_null(Util::getParameterClassName($dependency)) ? $this->resolvePrimitive($dependency) : $this->resolveClass($dependency); + $this->fireAfterResolvingAttributeCallbacks($dependency->getAttributes(), $result); + if ($dependency->isVariadic()) { $results = array_merge($results, $result); } else { @@ -1017,6 +1061,17 @@ 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. * @@ -1097,6 +1152,29 @@ protected function resolveVariadicClass(ReflectionParameter $parameter) return array_map(fn ($abstract) => $this->resolve($abstract), $concrete); } + /** + * Resolve a dependency based on an attribute. + * + * @param \ReflectionAttribute $attribute + * @return mixed + */ + protected function resolveFromAttribute(ReflectionAttribute $attribute) + { + $handler = $this->contextualAttributes[$attribute->getName()] ?? null; + + $instance = $attribute->newInstance(); + + if (is_null($handler) && method_exists($instance, 'resolve')) { + $handler = $instance->resolve(...); + } + + if (is_null($handler)) { + throw new BindingResolutionException("Contextual binding attribute [{$attribute->getName()}] has no registered handler."); + } + + return $handler($instance, $this); + } + /** * Throw an exception that the concrete is not instantiable. * @@ -1193,6 +1271,18 @@ public function afterResolving($abstract, ?Closure $callback = null) } } + /** + * Register a new after resolving attribute callback for all types. + * + * @param string $attribute + * @param \Closure $callback + * @return void + */ + public function afterResolvingAttribute(string $attribute, \Closure $callback) + { + $this->afterResolvingAttributeCallbacks[$attribute][] = $callback; + } + /** * Fire all of the before resolving callbacks. * @@ -1260,6 +1350,34 @@ protected function fireAfterResolvingCallbacks($abstract, $object) ); } + /** + * Fire all of the after resolving attribute callbacks. + * + * @param \ReflectionAttribute[] $abstract + * @param mixed $object + * @return void + */ + protected function fireAfterResolvingAttributeCallbacks(array $attributes, $object) + { + foreach ($attributes as $attribute) { + if (is_a($attribute->getName(), ContextualAttribute::class, true)) { + $instance = $attribute->newInstance(); + + if (method_exists($instance, 'after')) { + $instance->after($instance, $object, $this); + } + } + + $callbacks = $this->getCallbacksForType( + $attribute->getName(), $object, $this->afterResolvingAttributeCallbacks + ); + + foreach ($callbacks as $callback) { + $callback($attribute->newInstance(), $object, $this); + } + } + } + /** * Get all callbacks for a given type. * diff --git a/src/Illuminate/Contracts/Container/ContextualAttribute.php b/src/Illuminate/Contracts/Container/ContextualAttribute.php new file mode 100644 index 000000000000..06f6f06b8998 --- /dev/null +++ b/src/Illuminate/Contracts/Container/ContextualAttribute.php @@ -0,0 +1,8 @@ +afterResolvingAttribute(ContainerTestOnTenant::class, function (ContainerTestOnTenant $attribute, HasTenantImpl $hasTenantImpl, Container $container) { + $hasTenantImpl->onTenant($attribute->tenant); + }); + + $hasTenantA = $container->make(ContainerTestHasTenantImplPropertyWithTenantA::class); + $this->assertInstanceOf(HasTenantImpl::class, $hasTenantA->property); + $this->assertEquals(Tenant::TenantA, $hasTenantA->property->tenant); + + $hasTenantB = $container->make(ContainerTestHasTenantImplPropertyWithTenantB::class); + $this->assertInstanceOf(HasTenantImpl::class, $hasTenantB->property); + $this->assertEquals(Tenant::TenantB, $hasTenantB->property->tenant); + } + + public function testCallbackIsCalledAfterClassWithAttributeIsResolved() + { + $container = new Container(); + + $container->afterResolvingAttribute( + ContainerTestBootable::class, + fn ($_, $instance, Container $container) => method_exists($instance, 'booting') && $container->call([$instance, 'booting']) + ); + + $instance = $container->make(ContainerTestHasBootable::class); + + $this->assertInstanceOf(ContainerTestHasBootable::class, $instance); + $this->assertTrue($instance->hasBooted); + } + + public function testCallbackIsCalledAfterClassWithConstructorAndAttributeIsResolved() + { + $container = new Container(); + + $container->afterResolvingAttribute(ContainerTestConfiguresClass::class, function (ContainerTestConfiguresClass $attribute, $class) { + $class->value = $attribute->value; + }); + + $container->when(ContainerTestHasSelfConfiguringAttributeAndConstructor::class) + ->needs('$value') + ->give('no-the-right-value'); + + $instance = $container->make(ContainerTestHasSelfConfiguringAttributeAndConstructor::class); + + $this->assertInstanceOf(ContainerTestHasSelfConfiguringAttributeAndConstructor::class, $instance); + $this->assertEquals('the-right-value', $instance->value); + } +} + +#[Attribute(Attribute::TARGET_PARAMETER)] +final class ContainerTestOnTenant +{ + public function __construct( + public readonly Tenant $tenant + ) { + } +} + +enum Tenant +{ + case TenantA; + case TenantB; +} + +final class HasTenantImpl +{ + public ?Tenant $tenant = null; + + public function onTenant(Tenant $tenant): void + { + $this->tenant = $tenant; + } +} + +final class ContainerTestHasTenantImplPropertyWithTenantA +{ + public function __construct( + #[ContainerTestOnTenant(Tenant::TenantA)] + public readonly HasTenantImpl $property + ) { + } +} + +final class ContainerTestHasTenantImplPropertyWithTenantB +{ + public function __construct( + #[ContainerTestOnTenant(Tenant::TenantB)] + public readonly HasTenantImpl $property + ) { + } +} + +#[Attribute(Attribute::TARGET_CLASS)] +final class ContainerTestConfiguresClass +{ + public function __construct( + public readonly string $value + ) { + } +} + +#[ContainerTestConfiguresClass(value: 'the-right-value')] +final class ContainerTestHasSelfConfiguringAttributeAndConstructor +{ + public function __construct( + public string $value + ) { + } +} + +#[Attribute(Attribute::TARGET_CLASS)] +final class ContainerTestBootable +{ +} + +#[ContainerTestBootable] +final class ContainerTestHasBootable +{ + public bool $hasBooted = false; + + public function booting(): void + { + $this->hasBooted = true; + } +} diff --git a/tests/Container/ContextualAttributeBindingTest.php b/tests/Container/ContextualAttributeBindingTest.php new file mode 100644 index 000000000000..a474cbbba531 --- /dev/null +++ b/tests/Container/ContextualAttributeBindingTest.php @@ -0,0 +1,181 @@ +bind(ContainerTestContract::class, fn () => new ContainerTestImplB); + $container->whenHasAttribute(ContainerTestAttributeThatResolvesContractImpl::class, function (ContainerTestAttributeThatResolvesContractImpl $attribute) { + return match ($attribute->name) { + 'A' => new ContainerTestImplA, + 'B' => new ContainerTestImplB + }; + }); + + $classA = $container->make(ContainerTestHasAttributeThatResolvesToImplA::class); + + $this->assertInstanceOf(ContainerTestHasAttributeThatResolvesToImplA::class, $classA); + $this->assertInstanceOf(ContainerTestImplA::class, $classA->property); + + $classB = $container->make(ContainerTestHasAttributeThatResolvesToImplB::class); + + $this->assertInstanceOf(ContainerTestHasAttributeThatResolvesToImplB::class, $classB); + $this->assertInstanceOf(ContainerTestImplB::class, $classB->property); + } + + public function testScalarDependencyCanBeResolvedFromAttributeBinding() + { + $container = new Container; + $container->singleton('config', fn () => new Repository([ + 'app' => [ + 'timezone' => 'Europe/Paris', + ], + ])); + + $container->whenHasAttribute(ContainerTestConfigValue::class, function (ContainerTestConfigValue $attribute, Container $container) { + return $container->make('config')->get($attribute->key); + }); + + $class = $container->make(ContainerTestHasConfigValueProperty::class); + + $this->assertInstanceOf(ContainerTestHasConfigValueProperty::class, $class); + $this->assertEquals('Europe/Paris', $class->timezone); + } + + public function testScalarDependencyCanBeResolvedFromAttributeResolveMethod() + { + $container = new Container; + $container->singleton('config', fn () => new Repository([ + 'app' => [ + 'env' => 'production', + ], + ])); + + $class = $container->make(ContainerTestHasConfigValueWithResolveProperty::class); + + $this->assertInstanceOf(ContainerTestHasConfigValueWithResolveProperty::class, $class); + $this->assertEquals('production', $class->env); + } + + public function testDependencyWithAfterCallbackAttributeCanBeResolved() + { + $container = new Container; + + $class = $container->make(ContainerTestHasConfigValueWithResolvePropertyAndAfterCallback::class); + + $this->assertEquals('Developer', $class->person->role); + } +} + +#[Attribute(Attribute::TARGET_PARAMETER)] +class ContainerTestAttributeThatResolvesContractImpl implements ContextualAttribute +{ + public function __construct( + public readonly string $name + ) { + } +} + +interface ContainerTestContract +{ +} + +final class ContainerTestImplA implements ContainerTestContract +{ +} + +final class ContainerTestImplB implements ContainerTestContract +{ +} + +final class ContainerTestHasAttributeThatResolvesToImplA +{ + public function __construct( + #[ContainerTestAttributeThatResolvesContractImpl('A')] + public readonly ContainerTestContract $property + ) { + } +} + +final class ContainerTestHasAttributeThatResolvesToImplB +{ + public function __construct( + #[ContainerTestAttributeThatResolvesContractImpl('B')] + public readonly ContainerTestContract $property + ) { + } +} + +#[Attribute(Attribute::TARGET_PARAMETER)] +final class ContainerTestConfigValue implements ContextualAttribute +{ + public function __construct( + public readonly string $key + ) { + } +} + +final class ContainerTestHasConfigValueProperty +{ + public function __construct( + #[ContainerTestConfigValue('app.timezone')] + public string $timezone + ) { + } +} + +#[Attribute(Attribute::TARGET_PARAMETER)] +final class ContainerTestConfigValueWithResolve implements ContextualAttribute +{ + public function __construct( + public readonly string $key + ) { + } + + public function resolve(self $attribute, Container $container): string + { + return $container->make('config')->get($attribute->key); + } +} + +final class ContainerTestHasConfigValueWithResolveProperty +{ + public function __construct( + #[ContainerTestConfigValueWithResolve('app.env')] + public string $env + ) { + } +} + +#[Attribute(Attribute::TARGET_PARAMETER)] +final class ContainerTestConfigValueWithResolveAndAfter implements ContextualAttribute +{ + public function resolve(self $attribute, Container $container): object + { + return (object) ['name' => 'Taylor']; + } + + public function after(self $attribute, object $value, Container $container): void + { + $value->role = 'Developer'; + } +} + +final class ContainerTestHasConfigValueWithResolvePropertyAndAfterCallback +{ + public function __construct( + #[ContainerTestConfigValueWithResolveAndAfter] + public object $person + ) { + } +}