1
|
|
|
<?php |
2
|
|
|
namespace Hgraca\MicroDI; |
3
|
|
|
|
4
|
|
|
use Hgraca\Helper\InstanceHelper; |
5
|
|
|
use Hgraca\MicroDI\Exception\CanNotInstantiateDependenciesException; |
6
|
|
|
use Hgraca\MicroDI\Port\ContainerInterface; |
7
|
|
|
use InvalidArgumentException; |
8
|
|
|
|
9
|
|
|
final class Builder implements BuilderInterface |
10
|
|
|
{ |
11
|
|
|
/** @var ContainerInterface */ |
12
|
|
|
private $container; |
13
|
|
|
|
14
|
|
|
/** @var string[] */ |
15
|
|
|
private $dependentStack = []; |
16
|
|
|
|
17
|
|
|
/** @var string[] */ |
18
|
|
|
private $argumentNameStack = []; |
19
|
|
|
|
20
|
|
|
/** @var DependencyResolverInterface */ |
21
|
|
|
private $dependencyResolver; |
22
|
|
|
|
23
|
|
|
public function __construct(ContainerInterface $container, DependencyResolverInterface $dependencyResolver) |
24
|
|
|
{ |
25
|
|
|
$this->container = $container; |
26
|
|
|
$this->dependencyResolver = $dependencyResolver; |
27
|
|
|
} |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @throws CanNotInstantiateDependenciesException |
31
|
|
|
*/ |
32
|
|
|
public function build(string $class, array $arguments = []) |
33
|
|
|
{ |
34
|
|
|
if (! $this->container->hasInstance($class)) { |
35
|
|
|
$dependencies = $this->buildDependencies([$class, '__construct'], $arguments); |
36
|
|
|
|
37
|
|
|
$this->container->addInstance(InstanceHelper::createInstance($class, $dependencies)); |
|
|
|
|
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
return $this->container->getInstance($class); |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
public function buildFromFactory(string $factoryClass, array $arguments = []) |
44
|
|
|
{ |
45
|
|
|
if (! is_a($factoryClass, FactoryInterface::class, true)) { |
46
|
|
|
throw new InvalidArgumentException( |
47
|
|
|
"The given factory class $factoryClass must implement " . FactoryInterface::class |
48
|
|
|
); |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
/** @var FactoryInterface $factory */ |
52
|
|
|
$factory = $this->build($factoryClass, $arguments); |
53
|
|
|
|
54
|
|
|
$context = $this->container->hasFactoryContext($factoryClass) |
55
|
|
|
? $this->container->getFactoryContext($factoryClass) |
56
|
|
|
: []; |
57
|
|
|
|
58
|
|
|
return $factory->create($context); |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @throws CanNotInstantiateDependenciesException |
63
|
|
|
*/ |
64
|
|
|
public function buildDependencies(array $callable, array $arguments = []): array |
65
|
|
|
{ |
66
|
|
|
$dependentClass = is_string($callable[0]) ? $callable[0] : get_class($callable[0]); |
67
|
|
|
$dependentMethod = $callable[1]; |
68
|
|
|
|
69
|
|
|
$dependencies = $this->dependencyResolver->resolveDependencies($dependentClass, $dependentMethod); |
70
|
|
|
|
71
|
|
|
return $this->instantiateDependencies($dependentClass, $dependencies, $arguments); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @throws CanNotInstantiateDependenciesException |
76
|
|
|
*/ |
77
|
|
|
private function instantiateDependencies(string $dependentClass, array $parameters, array $arguments): array |
78
|
|
|
{ |
79
|
|
|
$dependencies = []; |
80
|
|
|
|
81
|
|
|
foreach ($parameters as $parameter) { |
82
|
|
|
$dependencies = $this->setParameterArgument($dependentClass, $arguments, $parameter); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
return $dependencies; |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* @throws CanNotInstantiateDependenciesException |
90
|
|
|
*/ |
91
|
|
|
private function setParameterArgument(string $dependentClass, array $arguments, array $parameter) |
92
|
|
|
{ |
93
|
|
|
$parameterName = $parameter['name']; |
94
|
|
|
switch (true) { |
95
|
|
|
case $this->dependencyIsInGivenArguments($parameterName, $arguments): |
96
|
|
|
$this->setDependencyFromArguments($dependencies, $parameterName, $arguments); |
97
|
|
|
break; |
98
|
|
|
case $this->dependencyIsClassOrInterface($parameter): |
99
|
|
|
$this->setDependencyRecursively($dependencies, $dependentClass, $parameterName, $parameter); |
100
|
|
|
break; |
101
|
|
|
case $this->dependencyNameIsInContainer($parameterName): |
102
|
|
|
$this->setDependencyFromParameterInContainer($parameterName, $dependencies); |
103
|
|
|
break; |
104
|
|
|
default: |
105
|
|
|
throw new CanNotInstantiateDependenciesException( |
106
|
|
|
"Could not instantiate dependency for class '$dependentClass', parameter '$parameterName'." |
107
|
|
|
); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
return $dependencies; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
private function dependencyIsInGivenArguments(string $parameterName, array $arguments): bool |
114
|
|
|
{ |
115
|
|
|
return array_key_exists($parameterName, $arguments); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
private function setDependencyFromArguments(array &$dependencies, string $parameterName, array $arguments) |
119
|
|
|
{ |
120
|
|
|
return $dependencies[$parameterName] = $arguments[$parameterName]; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
private function dependencyIsClassOrInterface(array $parameter): bool |
124
|
|
|
{ |
125
|
|
|
return isset($parameter['class']) && |
126
|
|
|
(class_exists($parameter['class']) || interface_exists($parameter['class'])); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
private function setDependencyRecursively( |
130
|
|
|
array &$dependencies, |
131
|
|
|
string $dependentClass, |
132
|
|
|
string $parameterName, |
133
|
|
|
array $parameter |
134
|
|
|
) { |
135
|
|
|
$this->pushDependentClass($dependentClass); |
136
|
|
|
$this->pushArgumentName($parameterName); |
137
|
|
|
$dependencies[$parameterName] = $this->build($parameter['class'], []); |
138
|
|
|
$this->popDependentClass(); |
139
|
|
|
$this->popArgumentName(); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
private function dependencyNameIsInContainer(string $parameterName): bool |
143
|
|
|
{ |
144
|
|
|
return $this->container->hasArgument($parameterName); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
private function setDependencyFromParameterInContainer(string $parameterName, array &$dependencies) |
148
|
|
|
{ |
149
|
|
|
$dependencies[$parameterName] = $this->container->getArgument($parameterName); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
private function pushDependentClass(string $dependentClass) |
153
|
|
|
{ |
154
|
|
|
$this->dependentStack[] = $dependentClass; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
private function popDependentClass(): string |
158
|
|
|
{ |
159
|
|
|
return array_pop($this->dependentStack); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
private function pushArgumentName(string $argumentName) |
163
|
|
|
{ |
164
|
|
|
$this->argumentNameStack[] = $argumentName; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
private function popArgumentName(): string |
168
|
|
|
{ |
169
|
|
|
return array_pop($this->argumentNameStack); |
170
|
|
|
} |
171
|
|
|
} |
172
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.