From 447b656e6a5849c0811fb21c9e2ba3aa5b21edd3 Mon Sep 17 00:00:00 2001 From: Peter Fox <peter.fox@peterfox.me> Date: Sat, 15 Feb 2025 12:18:44 +0000 Subject: [PATCH 1/2] Proof of concept --- src/Illuminate/Container/Container.php | 68 +++++++++++++++++-- .../Contracts/Container/Container.php | 18 ++--- tests/Container/ContainerTest.php | 18 +++++ 3 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index c6e4be09db1b..03621b1d20fe 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Container\CircularDependencyException; use Illuminate\Contracts\Container\Container as ContainerContract; use Illuminate\Contracts\Container\ContextualAttribute; +use Illuminate\Support\Collection; use LogicException; use ReflectionAttribute; use ReflectionClass; @@ -268,15 +269,27 @@ public function isAlias($name) /** * Register a binding with the container. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string|null $concrete * @param bool $shared * @return void * * @throws \TypeError + * @throws ReflectionException */ public function bind($abstract, $concrete = null, $shared = false) { + if ($abstract instanceof Closure) { + $abstracts = $this->closureReturnTypes($abstract); + $concrete = $abstract; + + foreach ($abstracts as $abstract) { + $this->bind($abstract, $concrete, $shared); + } + + return; + } + $this->dropStaleInstances($abstract); // If no concrete type was given, we will simply set the concrete type to the @@ -381,7 +394,7 @@ public function callMethodBinding($method, $instance) * Add a contextual binding to the container. * * @param string $concrete - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string $implementation * @return void */ @@ -393,7 +406,7 @@ public function addContextualBinding($concrete, $abstract, $implementation) /** * Register a binding if it hasn't already been registered. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string|null $concrete * @param bool $shared * @return void @@ -408,7 +421,7 @@ public function bindIf($abstract, $concrete = null, $shared = false) /** * Register a shared binding in the container. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string|null $concrete * @return void */ @@ -420,7 +433,7 @@ public function singleton($abstract, $concrete = null) /** * Register a shared binding if it hasn't already been registered. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string|null $concrete * @return void */ @@ -434,7 +447,7 @@ public function singletonIf($abstract, $concrete = null) /** * Register a scoped binding in the container. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string|null $concrete * @return void */ @@ -448,7 +461,7 @@ public function scoped($abstract, $concrete = null) /** * Register a scoped binding if it hasn't already been registered. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string|null $concrete * @return void */ @@ -1635,4 +1648,45 @@ public function __set($key, $value) { $this[$key] = $value; } + + /** + * Get the class names / types of the return type of the given Closure. + * + * @param \Closure $closure + * @return list<class-string> + * + * @throws \ReflectionException + */ + protected function closureReturnTypes(Closure $closure) + { + $reflection = new ReflectionFunction($closure); + + if ( + $reflection->getReturnType() === null || + $reflection->getReturnType() instanceof \ReflectionIntersectionType + ) { + return []; + } + + if ( + $reflection->getReturnType() instanceof \ReflectionUnionType + ) { + $types = $reflection->getReturnType()->getTypes(); + } else { + $types = [$reflection->getReturnType()]; + } + + return (new Collection($types)) + ->reject(function (\ReflectionNamedType $type) { + return $type->isBuiltin(); + }) + ->reject(function (\ReflectionNamedType $type) { + return in_array($type->getName(), ['static', 'self']); + }) + ->map(function (\ReflectionNamedType $type) { + return $type->getName(); + }) + ->values() + ->all(); + } } diff --git a/src/Illuminate/Contracts/Container/Container.php b/src/Illuminate/Contracts/Container/Container.php index acd5dcf5eb9b..c00ddf5cafdb 100644 --- a/src/Illuminate/Contracts/Container/Container.php +++ b/src/Illuminate/Contracts/Container/Container.php @@ -56,7 +56,7 @@ public function tagged($tag); /** * Register a binding with the container. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string|null $concrete * @param bool $shared * @return void @@ -75,7 +75,7 @@ public function bindMethod($method, $callback); /** * Register a binding if it hasn't already been registered. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string|null $concrete * @param bool $shared * @return void @@ -85,7 +85,7 @@ public function bindIf($abstract, $concrete = null, $shared = false); /** * Register a shared binding in the container. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string|null $concrete * @return void */ @@ -94,7 +94,7 @@ public function singleton($abstract, $concrete = null); /** * Register a shared binding if it hasn't already been registered. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string|null $concrete * @return void */ @@ -103,7 +103,7 @@ public function singletonIf($abstract, $concrete = null); /** * Register a scoped binding in the container. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string|null $concrete * @return void */ @@ -112,7 +112,7 @@ public function scoped($abstract, $concrete = null); /** * Register a scoped binding if it hasn't already been registered. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string|null $concrete * @return void */ @@ -121,7 +121,7 @@ public function scopedIf($abstract, $concrete = null); /** * "Extend" an abstract type in the container. * - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure $closure * @return void * @@ -134,7 +134,7 @@ public function extend($abstract, Closure $closure); * * @template TInstance of mixed * - * @param string $abstract + * @param \Closure|string $abstract * @param TInstance $instance * @return TInstance */ @@ -144,7 +144,7 @@ public function instance($abstract, $instance); * Add a contextual binding to the container. * * @param string $concrete - * @param string $abstract + * @param \Closure|string $abstract * @param \Closure|string $implementation * @return void */ diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index fbe4414521fe..2ef9189cba19 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -40,6 +40,24 @@ public function testClosureResolution() $this->assertSame('Taylor', $container->make('name')); } + public function testAbstractCanBeBoundFromConcreteReturnType() + { + $container = new Container; + $container->bind(function (): IContainerContractStub|ContainerImplementationStub { + return new ContainerImplementationStub; + }); + $container->singleton(function (): ContainerConcreteStub { + return new ContainerConcreteStub; + }); + + $this->assertInstanceOf( + IContainerContractStub::class, + $container->make(IContainerContractStub::class) + ); + + $this->assertTrue($container->isShared(ContainerConcreteStub::class)); + } + public function testBindIfDoesntRegisterIfServiceAlreadyRegistered() { $container = new Container; From d0d835ffc144c4d9b24edafe8133e439505a7c5c Mon Sep 17 00:00:00 2001 From: Taylor Otwell <taylor@laravel.com> Date: Mon, 17 Feb 2025 10:36:22 -0600 Subject: [PATCH 2/2] formatting --- src/Illuminate/Container/Container.php | 102 +++++++++++++------------ 1 file changed, 53 insertions(+), 49 deletions(-) diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 03621b1d20fe..bdfe7dd13cc5 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -15,7 +15,9 @@ use ReflectionClass; use ReflectionException; use ReflectionFunction; +use ReflectionIntersectionType; use ReflectionParameter; +use ReflectionUnionType; use TypeError; class Container implements ArrayAccess, ContainerContract @@ -280,14 +282,9 @@ public function isAlias($name) public function bind($abstract, $concrete = null, $shared = false) { if ($abstract instanceof Closure) { - $abstracts = $this->closureReturnTypes($abstract); - $concrete = $abstract; - - foreach ($abstracts as $abstract) { - $this->bind($abstract, $concrete, $shared); - } - - return; + return $this->bindBasedOnClosureReturnTypes( + $abstract, $concrete, $shared + ); } $this->dropStaleInstances($abstract); @@ -472,6 +469,54 @@ public function scopedIf($abstract, $concrete = null) } } + /** + * Register a binding with the container based on the given Closure's return types. + * + * @param \Closure|string $abstract + * @param \Closure|string|null $concrete + * @param bool $shared + * @return void + */ + protected function bindBasedOnClosureReturnTypes($abstract, $concrete = null, $shared = false) + { + $abstracts = $this->closureReturnTypes($abstract); + + $concrete = $abstract; + + foreach ($abstracts as $abstract) { + $this->bind($abstract, $concrete, $shared); + } + } + + /** + * Get the class names / types of the return type of the given Closure. + * + * @param \Closure $closure + * @return list<class-string> + * + * @throws \ReflectionException + */ + protected function closureReturnTypes(Closure $closure) + { + $reflection = new ReflectionFunction($closure); + + if ($reflection->getReturnType() === null || + $reflection->getReturnType() instanceof ReflectionIntersectionType) { + return []; + } + + $types = $reflection->getReturnType() instanceof ReflectionUnionType + ? $reflection->getReturnType()->getTypes() + : [$reflection->getReturnType()]; + + return (new Collection($types)) + ->reject(fn ($type) => $type->isBuiltin()) + ->reject(fn ($type) => in_array($type->getName(), ['static', 'self'])) + ->map(fn ($type) => $type->getName()) + ->values() + ->all(); + } + /** * "Extend" an abstract type in the container. * @@ -1648,45 +1693,4 @@ public function __set($key, $value) { $this[$key] = $value; } - - /** - * Get the class names / types of the return type of the given Closure. - * - * @param \Closure $closure - * @return list<class-string> - * - * @throws \ReflectionException - */ - protected function closureReturnTypes(Closure $closure) - { - $reflection = new ReflectionFunction($closure); - - if ( - $reflection->getReturnType() === null || - $reflection->getReturnType() instanceof \ReflectionIntersectionType - ) { - return []; - } - - if ( - $reflection->getReturnType() instanceof \ReflectionUnionType - ) { - $types = $reflection->getReturnType()->getTypes(); - } else { - $types = [$reflection->getReturnType()]; - } - - return (new Collection($types)) - ->reject(function (\ReflectionNamedType $type) { - return $type->isBuiltin(); - }) - ->reject(function (\ReflectionNamedType $type) { - return in_array($type->getName(), ['static', 'self']); - }) - ->map(function (\ReflectionNamedType $type) { - return $type->getName(); - }) - ->values() - ->all(); - } }