Skip to content

Commit a306193

Browse files
[11.x] fix: allows injection using multiple interfaces with the same concrete implementation (#53275)
* fix: allows injection of multiple interfaces with the same implementation * fix: ensure interfaces are only resolved once * fix: allows injection using multiple interfaces with the same concrete implementation * wip: working but ugly * test: ensure all tests pass * chore: fix code style issues * chore: fix code style issues * remove forgotten dd * wip * test: add test to ensure multiple interfaces with the same implementation can be injected in controllers * chore: remove unused imports * chore: adhere to style guidelines * formatting * formatting * formatting * formatting * formatting --------- Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent 83c3855 commit a306193

File tree

2 files changed

+111
-5
lines changed

2 files changed

+111
-5
lines changed

src/Illuminate/Routing/ResolvesRouteDependencies.php

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
namespace Illuminate\Routing;
44

55
use Illuminate\Container\Util;
6+
use Illuminate\Contracts\Container\BindingResolutionException;
67
use Illuminate\Support\Arr;
78
use Illuminate\Support\Reflector;
89
use ReflectionClass;
10+
use ReflectionException;
911
use ReflectionFunctionAbstract;
1012
use ReflectionMethod;
1113
use ReflectionParameter;
@@ -47,15 +49,24 @@ public function resolveMethodDependencies(array $parameters, ReflectionFunctionA
4749

4850
$skippableValue = new stdClass;
4951

52+
$resolvedInterfaces = [];
53+
5054
foreach ($reflector->getParameters() as $key => $parameter) {
51-
$instance = $this->transformDependency($parameter, $parameters, $skippableValue);
55+
$className = Reflector::getParameterClassName($parameter);
56+
57+
$instance = $this->transformDependency($parameter, $parameters, $className, $skippableValue, $resolvedInterfaces);
58+
59+
if ($instance !== $skippableValue &&
60+
! $this->alreadyInResolvedInterfaces($className, $resolvedInterfaces)) {
61+
$resolvedInterfaces[] = $className;
62+
}
5263

5364
if ($instance !== $skippableValue) {
5465
$instanceCount++;
5566

5667
$this->spliceIntoParameters($parameters, $key, $instance);
5768
} elseif (! isset($values[$key - $instanceCount]) &&
58-
$parameter->isDefaultValueAvailable()) {
69+
$parameter->isDefaultValueAvailable()) {
5970
$this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue());
6071
}
6172

@@ -68,18 +79,27 @@ public function resolveMethodDependencies(array $parameters, ReflectionFunctionA
6879
/**
6980
* Attempt to transform the given parameter into a class instance.
7081
*
71-
* @param \ReflectionParameter $parameter
82+
* @param ReflectionParameter $parameter
7283
* @param array $parameters
84+
* @param string $className
7385
* @param object $skippableValue
86+
* @param $resolvedInterfaces
7487
* @return mixed
88+
*
89+
* @throws BindingResolutionException
90+
* @throws ReflectionException
7591
*/
76-
protected function transformDependency(ReflectionParameter $parameter, $parameters, $skippableValue)
92+
protected function transformDependency(ReflectionParameter $parameter, $parameters, $className, object $skippableValue, $resolvedInterfaces)
7793
{
7894
if ($attribute = Util::getContextualAttributeFromDependency($parameter)) {
7995
return $this->container->resolveFromAttribute($attribute);
8096
}
8197

82-
$className = Reflector::getParameterClassName($parameter);
98+
if ($this->isSimilarConcreteToExistingParameterButDifferentInterface(
99+
$className, $parameters, $resolvedInterfaces
100+
)) {
101+
return $this->container->make($className);
102+
}
83103

84104
// If the parameter has a type-hinted class, we will check to see if it is already in
85105
// the list of parameters. If it is we will just skip it as it is probably a model
@@ -95,6 +115,24 @@ protected function transformDependency(ReflectionParameter $parameter, $paramete
95115
return $skippableValue;
96116
}
97117

118+
/**
119+
* Determines if an instance of the given class is already in the parameters, but the route is type-hinting another interface that hasn't yet been resolved.
120+
*
121+
* @param string $className
122+
* @param array $parameters
123+
* @param array $resolvedInterfaces
124+
* @return bool
125+
*/
126+
protected function isSimilarConcreteToExistingParameterButDifferentInterface($className, array $parameters, array $resolvedInterfaces)
127+
{
128+
// See: https://github.com/laravel/framework/pull/53275
129+
return $className &&
130+
$this->alreadyInParameters($className, $parameters) &&
131+
interface_exists($className) &&
132+
! $this->alreadyInResolvedInterfaces($className, $resolvedInterfaces) &&
133+
(new ReflectionClass($className))->isInterface();
134+
}
135+
98136
/**
99137
* Determine if an object of the given class is in a list of parameters.
100138
*
@@ -107,6 +145,22 @@ protected function alreadyInParameters($class, array $parameters)
107145
return ! is_null(Arr::first($parameters, fn ($value) => $value instanceof $class));
108146
}
109147

148+
/**
149+
* Determine if the given class name is already in the list of resolved interfaces.
150+
*
151+
* @param string|null $class
152+
* @param array $resolvedInterfaces
153+
* @return bool
154+
*/
155+
protected function alreadyInResolvedInterfaces($class, array $resolvedInterfaces)
156+
{
157+
if (! is_null($class)) {
158+
return in_array($class, $resolvedInterfaces);
159+
}
160+
161+
return false;
162+
}
163+
110164
/**
111165
* Splice the given value into the parameter list.
112166
*
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Routing;
4+
5+
use Illuminate\Container\Container;
6+
use Illuminate\Contracts\Routing\Registrar;
7+
use Illuminate\Events\Dispatcher;
8+
use Illuminate\Routing\Controller;
9+
use Illuminate\Routing\Router;
10+
use PHPUnit\Framework\TestCase;
11+
12+
class RouteDependencyInjectionTest extends TestCase
13+
{
14+
public function test_it_can_resolve_multiple_interfaces_with_the_same_implementation()
15+
{
16+
$container = new Container;
17+
$router = new Router(new Dispatcher, $container);
18+
$container->instance(Registrar::class, $router);
19+
20+
$container->bind(TestDependencyInterfaceA::class, TestDependencyImplementation::class);
21+
$container->bind(TestDependencyInterfaceB::class, TestDependencyImplementation::class);
22+
23+
$controller = new TestDependencyController();
24+
$result = $controller->index(
25+
$container->make(TestDependencyInterfaceA::class),
26+
$container->make(TestDependencyInterfaceB::class)
27+
);
28+
29+
$this->assertInstanceOf(TestDependencyImplementation::class, $result[0]);
30+
$this->assertInstanceOf(TestDependencyImplementation::class, $result[1]);
31+
}
32+
}
33+
34+
interface TestDependencyInterfaceA
35+
{
36+
}
37+
38+
interface TestDependencyInterfaceB
39+
{
40+
}
41+
42+
class TestDependencyImplementation implements TestDependencyInterfaceA, TestDependencyInterfaceB
43+
{
44+
}
45+
46+
class TestDependencyController extends Controller
47+
{
48+
public function index(TestDependencyInterfaceA $a, TestDependencyInterfaceB $b)
49+
{
50+
return [$a, $b];
51+
}
52+
}

0 commit comments

Comments
 (0)