Skip to content

Commit 834cf27

Browse files
[12.x] Bind abstract from concrete's return type (#54628)
* Proof of concept * formatting --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent dc63a5d commit 834cf27

File tree

3 files changed

+92
-16
lines changed

3 files changed

+92
-16
lines changed

src/Illuminate/Container/Container.php

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99
use Illuminate\Contracts\Container\CircularDependencyException;
1010
use Illuminate\Contracts\Container\Container as ContainerContract;
1111
use Illuminate\Contracts\Container\ContextualAttribute;
12+
use Illuminate\Support\Collection;
1213
use LogicException;
1314
use ReflectionAttribute;
1415
use ReflectionClass;
1516
use ReflectionException;
1617
use ReflectionFunction;
18+
use ReflectionIntersectionType;
1719
use ReflectionParameter;
20+
use ReflectionUnionType;
1821
use TypeError;
1922

2023
class Container implements ArrayAccess, ContainerContract
@@ -268,15 +271,22 @@ public function isAlias($name)
268271
/**
269272
* Register a binding with the container.
270273
*
271-
* @param string $abstract
274+
* @param \Closure|string $abstract
272275
* @param \Closure|string|null $concrete
273276
* @param bool $shared
274277
* @return void
275278
*
276279
* @throws \TypeError
280+
* @throws ReflectionException
277281
*/
278282
public function bind($abstract, $concrete = null, $shared = false)
279283
{
284+
if ($abstract instanceof Closure) {
285+
return $this->bindBasedOnClosureReturnTypes(
286+
$abstract, $concrete, $shared
287+
);
288+
}
289+
280290
$this->dropStaleInstances($abstract);
281291

282292
// If no concrete type was given, we will simply set the concrete type to the
@@ -381,7 +391,7 @@ public function callMethodBinding($method, $instance)
381391
* Add a contextual binding to the container.
382392
*
383393
* @param string $concrete
384-
* @param string $abstract
394+
* @param \Closure|string $abstract
385395
* @param \Closure|string $implementation
386396
* @return void
387397
*/
@@ -393,7 +403,7 @@ public function addContextualBinding($concrete, $abstract, $implementation)
393403
/**
394404
* Register a binding if it hasn't already been registered.
395405
*
396-
* @param string $abstract
406+
* @param \Closure|string $abstract
397407
* @param \Closure|string|null $concrete
398408
* @param bool $shared
399409
* @return void
@@ -408,7 +418,7 @@ public function bindIf($abstract, $concrete = null, $shared = false)
408418
/**
409419
* Register a shared binding in the container.
410420
*
411-
* @param string $abstract
421+
* @param \Closure|string $abstract
412422
* @param \Closure|string|null $concrete
413423
* @return void
414424
*/
@@ -420,7 +430,7 @@ public function singleton($abstract, $concrete = null)
420430
/**
421431
* Register a shared binding if it hasn't already been registered.
422432
*
423-
* @param string $abstract
433+
* @param \Closure|string $abstract
424434
* @param \Closure|string|null $concrete
425435
* @return void
426436
*/
@@ -434,7 +444,7 @@ public function singletonIf($abstract, $concrete = null)
434444
/**
435445
* Register a scoped binding in the container.
436446
*
437-
* @param string $abstract
447+
* @param \Closure|string $abstract
438448
* @param \Closure|string|null $concrete
439449
* @return void
440450
*/
@@ -448,7 +458,7 @@ public function scoped($abstract, $concrete = null)
448458
/**
449459
* Register a scoped binding if it hasn't already been registered.
450460
*
451-
* @param string $abstract
461+
* @param \Closure|string $abstract
452462
* @param \Closure|string|null $concrete
453463
* @return void
454464
*/
@@ -459,6 +469,54 @@ public function scopedIf($abstract, $concrete = null)
459469
}
460470
}
461471

472+
/**
473+
* Register a binding with the container based on the given Closure's return types.
474+
*
475+
* @param \Closure|string $abstract
476+
* @param \Closure|string|null $concrete
477+
* @param bool $shared
478+
* @return void
479+
*/
480+
protected function bindBasedOnClosureReturnTypes($abstract, $concrete = null, $shared = false)
481+
{
482+
$abstracts = $this->closureReturnTypes($abstract);
483+
484+
$concrete = $abstract;
485+
486+
foreach ($abstracts as $abstract) {
487+
$this->bind($abstract, $concrete, $shared);
488+
}
489+
}
490+
491+
/**
492+
* Get the class names / types of the return type of the given Closure.
493+
*
494+
* @param \Closure $closure
495+
* @return list<class-string>
496+
*
497+
* @throws \ReflectionException
498+
*/
499+
protected function closureReturnTypes(Closure $closure)
500+
{
501+
$reflection = new ReflectionFunction($closure);
502+
503+
if ($reflection->getReturnType() === null ||
504+
$reflection->getReturnType() instanceof ReflectionIntersectionType) {
505+
return [];
506+
}
507+
508+
$types = $reflection->getReturnType() instanceof ReflectionUnionType
509+
? $reflection->getReturnType()->getTypes()
510+
: [$reflection->getReturnType()];
511+
512+
return (new Collection($types))
513+
->reject(fn ($type) => $type->isBuiltin())
514+
->reject(fn ($type) => in_array($type->getName(), ['static', 'self']))
515+
->map(fn ($type) => $type->getName())
516+
->values()
517+
->all();
518+
}
519+
462520
/**
463521
* "Extend" an abstract type in the container.
464522
*

src/Illuminate/Contracts/Container/Container.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function tagged($tag);
5656
/**
5757
* Register a binding with the container.
5858
*
59-
* @param string $abstract
59+
* @param \Closure|string $abstract
6060
* @param \Closure|string|null $concrete
6161
* @param bool $shared
6262
* @return void
@@ -75,7 +75,7 @@ public function bindMethod($method, $callback);
7575
/**
7676
* Register a binding if it hasn't already been registered.
7777
*
78-
* @param string $abstract
78+
* @param \Closure|string $abstract
7979
* @param \Closure|string|null $concrete
8080
* @param bool $shared
8181
* @return void
@@ -85,7 +85,7 @@ public function bindIf($abstract, $concrete = null, $shared = false);
8585
/**
8686
* Register a shared binding in the container.
8787
*
88-
* @param string $abstract
88+
* @param \Closure|string $abstract
8989
* @param \Closure|string|null $concrete
9090
* @return void
9191
*/
@@ -94,7 +94,7 @@ public function singleton($abstract, $concrete = null);
9494
/**
9595
* Register a shared binding if it hasn't already been registered.
9696
*
97-
* @param string $abstract
97+
* @param \Closure|string $abstract
9898
* @param \Closure|string|null $concrete
9999
* @return void
100100
*/
@@ -103,7 +103,7 @@ public function singletonIf($abstract, $concrete = null);
103103
/**
104104
* Register a scoped binding in the container.
105105
*
106-
* @param string $abstract
106+
* @param \Closure|string $abstract
107107
* @param \Closure|string|null $concrete
108108
* @return void
109109
*/
@@ -112,7 +112,7 @@ public function scoped($abstract, $concrete = null);
112112
/**
113113
* Register a scoped binding if it hasn't already been registered.
114114
*
115-
* @param string $abstract
115+
* @param \Closure|string $abstract
116116
* @param \Closure|string|null $concrete
117117
* @return void
118118
*/
@@ -121,7 +121,7 @@ public function scopedIf($abstract, $concrete = null);
121121
/**
122122
* "Extend" an abstract type in the container.
123123
*
124-
* @param string $abstract
124+
* @param \Closure|string $abstract
125125
* @param \Closure $closure
126126
* @return void
127127
*
@@ -134,7 +134,7 @@ public function extend($abstract, Closure $closure);
134134
*
135135
* @template TInstance of mixed
136136
*
137-
* @param string $abstract
137+
* @param \Closure|string $abstract
138138
* @param TInstance $instance
139139
* @return TInstance
140140
*/
@@ -144,7 +144,7 @@ public function instance($abstract, $instance);
144144
* Add a contextual binding to the container.
145145
*
146146
* @param string $concrete
147-
* @param string $abstract
147+
* @param \Closure|string $abstract
148148
* @param \Closure|string $implementation
149149
* @return void
150150
*/

tests/Container/ContainerTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ public function testClosureResolution()
4040
$this->assertSame('Taylor', $container->make('name'));
4141
}
4242

43+
public function testAbstractCanBeBoundFromConcreteReturnType()
44+
{
45+
$container = new Container;
46+
$container->bind(function (): IContainerContractStub|ContainerImplementationStub {
47+
return new ContainerImplementationStub;
48+
});
49+
$container->singleton(function (): ContainerConcreteStub {
50+
return new ContainerConcreteStub;
51+
});
52+
53+
$this->assertInstanceOf(
54+
IContainerContractStub::class,
55+
$container->make(IContainerContractStub::class)
56+
);
57+
58+
$this->assertTrue($container->isShared(ContainerConcreteStub::class));
59+
}
60+
4361
public function testBindIfDoesntRegisterIfServiceAlreadyRegistered()
4462
{
4563
$container = new Container;

0 commit comments

Comments
 (0)