PluginProbe
Site Reviews / trunk
Site Reviews vtrunk
8.3.1 8.3.0 8.2.2 8.2.1 8.2.0 8.1.0 8.0.13 8.0.12 8.0.11 trunk 1.2.2 2.17.1 3.5.4 4.7.0 5.25.1 6.11.8 7.0.10 7.0.11 7.0.12 7.0.13 7.0.14 7.0.15 7.0.16 7.0.17 7.0.18 All 54 releases
site-reviews / plugin / Container.php

Container.php in Site Reviews trunk, at plugin/Container.php

270 lines 7.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace GeminiLabs\SiteReviews;
4
5 use GeminiLabs\SiteReviews\Exceptions\BindingResolutionException;
6 use GeminiLabs\SiteReviews\Helpers\Arr;
7 use GeminiLabs\SiteReviews\Helpers\Str;
8
9 abstract class Container
10 {
11 /**
12 * The container's bindings.
13 *
14 * @var array[]
15 */
16 protected array $bindings = [];
17 /**
18 * The stack of concretions currently being built.
19 */
20 protected array $buildStack = [];
21 /**
22 * The container's shared instances.
23 *
24 * @var object[]
25 */
26 protected array $instances = [];
27 /**
28 * The parameter override stack.
29 *
30 * @var array[]
31 */
32 protected array $with = [];
33
34 /**
35 * @param mixed $concrete
36 */
37 public function alias(string $alias, $concrete): void
38 {
39 $this->instances[$alias] = $concrete;
40 }
41
42 /**
43 * @param mixed $concrete
44 */
45 public function bind(string $abstract, $concrete = null, bool $shared = false): void
46 {
47 $this->dropStaleInstances($abstract);
48 if (is_null($concrete)) {
49 $concrete = $abstract;
50 }
51 if (!$concrete instanceof \Closure) {
52 $concrete = $this->getClosure($abstract, $concrete);
53 }
54 $this->bindings[$abstract] = compact('concrete', 'shared');
55 }
56
57 /**
58 * @param mixed $abstract
59 *
60 * @return mixed
61 */
62 public function make($abstract, array $parameters = [])
63 {
64 if (is_string($abstract) && !class_exists($abstract)) {
65 $alias = __NAMESPACE__.'\\'.Str::removePrefix($abstract, __NAMESPACE__);
66 if (class_exists($alias)) {
67 $abstract = $alias;
68 }
69 }
70 return $this->resolve($abstract, $parameters);
71 }
72
73 /**
74 * @param mixed $concrete
75 */
76 public function singleton(string $abstract, $concrete = null): void
77 {
78 if (isset($this->bindings[$abstract]['shared']) && $this->bindings[$abstract]['shared']) {
79 return; // Singleton already exists
80 }
81 $this->bind($abstract, $concrete, true);
82 }
83
84 /**
85 * @param \Closure|string $concrete
86 *
87 * @return mixed
88 *
89 * @throws BindingResolutionException
90 */
91 protected function construct($concrete)
92 {
93 if ($concrete instanceof \Closure) {
94 return $concrete($this, $this->getLastParameterOverride()); // probably a bound closure
95 }
96 try {
97 $reflector = new \ReflectionClass($concrete); // class or classname provided
98 } catch (\ReflectionException $e) {
99 throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
100 }
101 if (!$reflector->isInstantiable()) {
102 $this->throwNotInstantiable($concrete); // not an instantiable class
103 }
104 $this->buildStack[] = $concrete;
105 if (is_null($constructor = $reflector->getConstructor())) {
106 array_pop($this->buildStack);
107 return new $concrete(); // class has no __construct
108 }
109 try {
110 $instances = $this->resolveDependencies($constructor->getParameters()); // resolve class dependencies
111 } catch (BindingResolutionException $e) {
112 array_pop($this->buildStack);
113 throw $e;
114 }
115 array_pop($this->buildStack);
116 return $reflector->newInstanceArgs($instances); // return a new class
117 }
118
119 protected function dropStaleInstances(string $abstract): void
120 {
121 unset($this->instances[$abstract]);
122 }
123
124 /**
125 * The plugin's constructor dependencies are always single named classes,
126 * never union or intersection types.
127 *
128 * @return \ReflectionNamedType|null
129 */
130 protected function getClass(\ReflectionParameter $parameter)
131 {
132 return $parameter->getType(); // @phpstan-ignore return.type
133 }
134
135 protected function getClosure($abstract, $concrete): \Closure
136 {
137 return function ($container, $parameters = []) use ($abstract, $concrete) {
138 return $abstract == $concrete
139 ? $container->construct($concrete)
140 : $container->resolve($concrete, $parameters);
141 };
142 }
143
144 /**
145 * @return mixed
146 */
147 protected function getConcrete(string $abstract)
148 {
149 if (isset($this->bindings[$abstract])) {
150 return $this->bindings[$abstract]['concrete'];
151 }
152 return $abstract;
153 }
154
155 protected function getLastParameterOverride(): array
156 {
157 return Arr::consolidate(end($this->with));
158 }
159
160 /**
161 * @return mixed
162 */
163 protected function getParameterOverride(\ReflectionParameter $dependency)
164 {
165 return $this->getLastParameterOverride()[$dependency->name];
166 }
167
168 protected function hasParameterOverride(\ReflectionParameter $dependency): bool
169 {
170 return array_key_exists($dependency->name, $this->getLastParameterOverride());
171 }
172
173 protected function isShared(string $abstract): bool
174 {
175 return isset($this->instances[$abstract]) || !empty($this->bindings[$abstract]['shared']);
176 }
177
178 /**
179 * @param mixed $abstract
180 *
181 * @return mixed
182 *
183 * @throws BindingResolutionException
184 */
185 protected function resolve($abstract, array $parameters = [])
186 {
187 if (isset($this->instances[$abstract]) && empty($parameters)) {
188 return $this->instances[$abstract]; // return an existing singleton
189 }
190 $this->with[] = $parameters;
191 // getConcrete() answers a Closure (bind() wraps every concrete in one)
192 // or the abstract itself; construct() builds either
193 $object = $this->construct($this->getConcrete($abstract));
194 if ($this->isShared($abstract) && empty($parameters)) {
195 $this->instances[$abstract] = $object; // store as a singleton
196 }
197 array_pop($this->with);
198 return $object;
199 }
200
201 /**
202 * Resolve a class based dependency from the container.
203 *
204 * @return mixed
205 *
206 * @throws \Exception
207 */
208 protected function resolveClass(\ReflectionParameter $parameter)
209 {
210 try {
211 return $this->make($this->getClass($parameter)->getName());
212 } catch (\Exception $error) {
213 if ($parameter->isOptional()) {
214 return $parameter->getDefaultValue();
215 }
216 throw $error;
217 }
218 }
219
220 protected function resolveDependencies(array $dependencies): array
221 {
222 $results = [];
223 foreach ($dependencies as $dependency) {
224 if ($this->hasParameterOverride($dependency)) {
225 $results[] = $this->getParameterOverride($dependency);
226 continue;
227 }
228 $results[] = is_null($this->getClass($dependency))
229 ? $this->resolvePrimitive($dependency)
230 : $this->resolveClass($dependency);
231 }
232 return $results;
233 }
234
235 /**
236 * @return mixed
237 *
238 * @throws BindingResolutionException
239 */
240 protected function resolvePrimitive(\ReflectionParameter $parameter)
241 {
242 if ($parameter->isDefaultValueAvailable()) {
243 return $parameter->getDefaultValue();
244 }
245 $this->throwUnresolvablePrimitive($parameter);
246 }
247
248 /**
249 * @throws BindingResolutionException
250 */
251 protected function throwNotInstantiable(string $concrete): void
252 {
253 if (empty($this->buildStack)) {
254 $message = "Target [$concrete] is not instantiable.";
255 } else {
256 $previous = implode(', ', $this->buildStack);
257 $message = "Target [$concrete] is not instantiable while building [$previous].";
258 }
259 throw new BindingResolutionException($message);
260 }
261
262 /**
263 * @throws BindingResolutionException
264 */
265 protected function throwUnresolvablePrimitive(\ReflectionParameter $parameter): void
266 {
267 throw new BindingResolutionException("Unresolvable dependency resolving [$parameter] in class {$parameter->getDeclaringClass()->getName()}");
268 }
269 }
270