Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.64% |
159 / 168 |
|
69.23% |
9 / 13 |
CRAP | |
0.00% |
0 / 1 |
DiContainer | |
94.64% |
159 / 168 |
|
69.23% |
9 / 13 |
74.84 | |
0.00% |
0 / 1 |
getMappingItem | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
6.03 | |||
canSetValue | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
5.93 | |||
getItemFromPropertyType | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
generateParameterValues | |
100.00% |
56 / 56 |
|
100.00% |
1 / 1 |
22 | |||
createFromClassName | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
setMembers | |
90.00% |
18 / 20 |
|
0.00% |
0 / 1 |
7.05 | |||
create | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 | |||
has | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
get | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
new | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
7 | |||
call | |
86.36% |
19 / 22 |
|
0.00% |
0 / 1 |
9.21 | |||
clone | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
disposeImpl | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace PeServer\Core\DI; |
6 | |
7 | use ReflectionClass; |
8 | use ReflectionFunction; |
9 | use ReflectionFunctionAbstract; |
10 | use ReflectionMethod; |
11 | use ReflectionNamedType; |
12 | use ReflectionParameter; |
13 | use ReflectionProperty; |
14 | use ReflectionType; |
15 | use ReflectionUnionType; |
16 | use TypeError; |
17 | use PeServer\Core\Collection\Arr; |
18 | use PeServer\Core\DI\DiItem; |
19 | use PeServer\Core\DI\Inject; |
20 | use PeServer\Core\DI\IScopedDiContainer; |
21 | use PeServer\Core\DI\ScopedDiContainer; |
22 | use PeServer\Core\DisposerBase; |
23 | use PeServer\Core\IDisposable; |
24 | use PeServer\Core\ReflectionUtility; |
25 | use PeServer\Core\Text; |
26 | use PeServer\Core\Throws\DiContainerArgumentException; |
27 | use PeServer\Core\Throws\DiContainerNotFoundException; |
28 | use PeServer\Core\Throws\DiContainerUndefinedTypeException; |
29 | use PeServer\Core\Throws\NotImplementedException; |
30 | use PeServer\Core\TypeUtility; |
31 | |
32 | /** |
33 | * DIコンテナ実装。 |
34 | */ |
35 | class DiContainer extends DisposerBase implements IDiContainer |
36 | { |
37 | #region variable |
38 | |
39 | /** |
40 | * IDとの紐づけ。 |
41 | * |
42 | * @var array<class-string|non-empty-string,DiItem> |
43 | */ |
44 | protected array $mapping = []; |
45 | |
46 | #endregion |
47 | |
48 | #region function |
49 | |
50 | /** |
51 | * 登録アイテムを具象クラス名も考慮して取得する。 |
52 | * |
53 | * @param class-string|non-empty-string $idOrClassName 登録アイテムID |
54 | * @param bool $mappingKeyOnly 真の場合は登録アイテムIDのみに限定。偽の場合、登録されている具象クラス名を考慮する。 |
55 | * @return DiItem|null |
56 | */ |
57 | protected function getMappingItem(string $idOrClassName, bool $mappingKeyOnly): ?DiItem |
58 | { |
59 | if ($this->has($idOrClassName)) { |
60 | return $this->mapping[$idOrClassName]; |
61 | } |
62 | if ($mappingKeyOnly) { |
63 | return null; |
64 | } |
65 | |
66 | foreach ($this->mapping as $item) { |
67 | if ($item->type !== DiItem::TYPE_TYPE) { |
68 | continue; |
69 | } |
70 | |
71 | $itemTypeName = (string)$item->data; |
72 | if ($itemTypeName === $idOrClassName) { |
73 | return $item; |
74 | } |
75 | } |
76 | |
77 | return null; |
78 | } |
79 | |
80 | protected function canSetValue(?ReflectionType $parameterType, mixed $value): bool |
81 | { |
82 | if ($value === null) { |
83 | return false; |
84 | } |
85 | if (!is_object($value)) { |
86 | return false; |
87 | } |
88 | |
89 | foreach (ReflectionUtility::getTypes($parameterType) as $currentType) { |
90 | $typeName = $currentType->getName(); |
91 | if (is_a($value, $typeName)) { |
92 | return true; |
93 | } |
94 | } |
95 | |
96 | return false; |
97 | } |
98 | |
99 | protected function getItemFromPropertyType(?ReflectionType $parameterType, bool $mappingKeyOnly): ?DiItem |
100 | { |
101 | foreach (ReflectionUtility::getTypes($parameterType) as $currentType) { |
102 | /** @var class-string */ |
103 | $typeName = $currentType->getName(); |
104 | $item = $this->getMappingItem($typeName, $mappingKeyOnly); |
105 | if ($item !== null) { |
106 | return $item; |
107 | } |
108 | } |
109 | |
110 | return null; |
111 | } |
112 | |
113 | /** |
114 | * 生成オブジェクトに対するパラメータ一覧を生成する。 |
115 | * |
116 | * @param ReflectionFunctionAbstract $reflectionMethod |
117 | * @param array<int|string,mixed> $arguments `IDiContainer::new` 参照。 |
118 | * @param int $level 現在階層(0: 最初) |
119 | * @param bool $mappingKeyOnly 真の場合は登録アイテムIDのみに限定。偽の場合、登録されている具象クラス名を考慮する。 |
120 | * @param DiItem[] $callStack |
121 | * @return array<mixed> |
122 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) |
123 | */ |
124 | protected function generateParameterValues(ReflectionFunctionAbstract $reflectionMethod, array $arguments, int $level, bool $mappingKeyOnly, array $callStack): array |
125 | { |
126 | $result = []; |
127 | |
128 | $canDynamicArgument = !empty($arguments); |
129 | $dynamicArgumentKeys = $canDynamicArgument ? array_filter($arguments, fn ($k) => is_int($k) && $k < 0, ARRAY_FILTER_USE_KEY) : []; |
130 | if ($canDynamicArgument && !empty($dynamicArgumentKeys)) { |
131 | $dynamicArgumentKeys = array_keys($dynamicArgumentKeys); |
132 | rsort($dynamicArgumentKeys, SORT_NUMERIC); |
133 | } |
134 | |
135 | foreach ($reflectionMethod->getParameters() as $parameter) { |
136 | $parameterType = $parameter->getType(); |
137 | |
138 | /** @var DiItem|null */ |
139 | $item = null; |
140 | |
141 | // 引数指定 |
142 | if (!empty($arguments)) { |
143 | $isHit = false; |
144 | $argument = null; |
145 | if (isset($arguments[$parameter->getPosition()])) { |
146 | // 引数位置指定 |
147 | $isHit = true; |
148 | $argument = $arguments[$parameter->getPosition()]; |
149 | } else { |
150 | $parameterName = '$' . $parameter->name; |
151 | if (isset($arguments[$parameterName])) { |
152 | // 引数名 |
153 | $isHit = true; |
154 | $argument = $arguments[$parameterName]; |
155 | } else { |
156 | // 型名 |
157 | $types = ReflectionUtility::getTypes($parameterType); |
158 | foreach ($types as $type) { |
159 | if (isset($arguments[$type->getName()])) { |
160 | $isHit = true; |
161 | $argument = $arguments[$type->getName()]; |
162 | unset($arguments[$type->getName()]); |
163 | break; |
164 | } |
165 | } |
166 | } |
167 | } |
168 | |
169 | // -1 以下の割り当て可能パラメータを適用 |
170 | if (!$isHit && $canDynamicArgument) { |
171 | foreach ($dynamicArgumentKeys as $i => $key) { |
172 | if ($this->canSetValue($parameterType, $arguments[$key])) { |
173 | $argument = $arguments[$key]; |
174 | $isHit = true; |
175 | unset($dynamicArgumentKeys[$i]); |
176 | $canDynamicArgument = !empty($dynamicArgumentKeys); |
177 | break; |
178 | } |
179 | } |
180 | } |
181 | |
182 | if ($isHit) { |
183 | $result[$parameter->getPosition()] = $argument; |
184 | continue; |
185 | } |
186 | } |
187 | |
188 | // 属性指定 |
189 | $attributes = $parameter->getAttributes(Inject::class); |
190 | if (!Arr::isNullOrEmpty($attributes)) { |
191 | /** @var Inject */ |
192 | $attribute = $attributes[0]->newInstance(); |
193 | if (!Text::isNullOrWhiteSpace($attribute->id)) { |
194 | $id = $attribute->id; |
195 | $item = $this->getMappingItem($id, $mappingKeyOnly); |
196 | } |
197 | } |
198 | |
199 | if ($item === null) { |
200 | $item = $this->getItemFromPropertyType($parameterType, $mappingKeyOnly); |
201 | } |
202 | |
203 | // 未登録 |
204 | if ($item === null) { |
205 | if ($parameter->isDefaultValueAvailable()) { |
206 | $result[$parameter->getPosition()] = $parameter->getDefaultValue(); |
207 | continue; |
208 | } |
209 | if ($parameter->allowsNull()) { |
210 | $result[$parameter->getPosition()] = null; |
211 | continue; |
212 | } |
213 | |
214 | throw new DiContainerUndefinedTypeException($parameter->name); |
215 | } |
216 | |
217 | $parameterValue = $this->create($item, [], $level + 1, $mappingKeyOnly, [...$callStack, $item]); |
218 | $result[$parameter->getPosition()] = $parameterValue; |
219 | } |
220 | |
221 | return $result; |
222 | } |
223 | |
224 | /** |
225 | * クラス名からオブジェクトの生成。 |
226 | * |
227 | * @param class-string $className |
228 | * @param array<int|string,mixed> $arguments `IDiContainer::new` 参照。 |
229 | * @param int $level 現在階層(0: 最初) |
230 | * @param bool $mappingKeyOnly 真の場合は登録アイテムIDのみに限定。偽の場合、登録されている具象クラス名を考慮する。 |
231 | * @param DiItem[] $callStack |
232 | * @return mixed |
233 | */ |
234 | protected function createFromClassName(string $className, array $arguments, int $level, bool $mappingKeyOnly, array $callStack): mixed |
235 | { |
236 | $classReflection = new ReflectionClass($className); |
237 | $constructor = $classReflection->getConstructor(); |
238 | if ($constructor === null) { |
239 | return new $className(); |
240 | } |
241 | |
242 | $parameters = $this->generateParameterValues($constructor, $arguments, $level, $mappingKeyOnly, $callStack); |
243 | |
244 | $result = new $className(...$parameters); |
245 | $this->setMembers($result, $level, $mappingKeyOnly, $callStack); |
246 | |
247 | return $result; |
248 | } |
249 | |
250 | /** |
251 | * メンバ インジェクション |
252 | * |
253 | * @param object $target |
254 | * @param int $level 現在階層(0: 最初) |
255 | * @param bool $mappingKeyOnly 真の場合は登録アイテムIDのみに限定。偽の場合、登録されている具象クラス名を考慮する。 |
256 | * @param DiItem[] $callStack |
257 | */ |
258 | protected function setMembers(object $target, int $level, bool $mappingKeyOnly, array $callStack): void |
259 | { |
260 | $reflectionClass = new ReflectionClass($target); |
261 | |
262 | // $properties = $reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE); |
263 | |
264 | // $parent = $reflectionClass; |
265 | // while ($parent = $parent->getParentClass()) { |
266 | // $parentProperties = $parent->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE); |
267 | // $properties = array_merge($properties, $parentProperties); //いっつもわからんくなる。何が正しいのか |
268 | // } |
269 | $properties = ReflectionUtility::getAllProperties($reflectionClass); |
270 | |
271 | foreach ($properties as $property) { |
272 | // コンストラクタからプロパティになりあがっている場合はそっとしておく |
273 | if ($property->isPromoted()) { |
274 | continue; |
275 | } |
276 | |
277 | $attributes = $property->getAttributes(Inject::class); |
278 | |
279 | if (!Arr::isNullOrEmpty($attributes)) { |
280 | /** @var DiItem|null */ |
281 | $item = null; |
282 | |
283 | /** @var Inject */ |
284 | $attribute = $attributes[0]->newInstance(); |
285 | if (!Text::isNullOrWhiteSpace($attribute->id)) { |
286 | $id = $attribute->id; |
287 | $item = $this->getMappingItem($id, $mappingKeyOnly); |
288 | } |
289 | |
290 | if ($item === null) { |
291 | $propertyType = $property->getType(); |
292 | $item = $this->getItemFromPropertyType($propertyType, $mappingKeyOnly); |
293 | } |
294 | |
295 | // 設定できない場合は何もしない |
296 | if ($item !== null) { |
297 | $callStack[] = $item; |
298 | $propertyValue = $this->create($item, [], $level + 1, $mappingKeyOnly, $callStack); |
299 | $property->setAccessible(true); |
300 | $property->setValue($target, $propertyValue); |
301 | } |
302 | } |
303 | } |
304 | } |
305 | |
306 | /** |
307 | * 生成処理。 |
308 | * |
309 | * @param DiItem $item |
310 | * @param array<int|string,mixed> $arguments `IDiContainer::new` 参照。 |
311 | * @param int $level 現在階層(0: 最初) |
312 | * @param bool $mappingKeyOnly 真の場合は登録アイテムIDのみに限定。偽の場合、登録されている具象クラス名を考慮する。 |
313 | * @param DiItem[] $callStack |
314 | * @return mixed |
315 | */ |
316 | protected function create(DiItem $item, array $arguments, int $level, bool $mappingKeyOnly, array $callStack): mixed |
317 | { |
318 | // 値は何も考えなくていい |
319 | if ($item->type === DiItem::TYPE_VALUE) { |
320 | return $item->data; |
321 | } |
322 | |
323 | // 既にシングルトンデータを持っているならそのまま返却 |
324 | if ($item->lifecycle === DiItem::LIFECYCLE_SINGLETON && $item->hasSingletonValue()) { |
325 | return $item->getSingletonValue(); |
326 | } |
327 | |
328 | $result = null; |
329 | |
330 | if ($item->type === DiItem::TYPE_TYPE) { |
331 | /** @var class-string */ |
332 | $className = (string)$item->data; |
333 | $result = $this->createFromClassName($className, $arguments, $level, $mappingKeyOnly, $callStack); |
334 | } else { |
335 | assert($item->type === DiItem::TYPE_FACTORY); //@phpstan-ignore-line |
336 | $result = call_user_func($item->data, $this, $callStack); |
337 | } |
338 | |
339 | if (is_object($result)) { |
340 | $this->setMembers($result, $level, $mappingKeyOnly, $callStack); |
341 | } |
342 | |
343 | if ($item->lifecycle === DiItem::LIFECYCLE_SINGLETON) { |
344 | $item->setSingletonValue($result); |
345 | } |
346 | |
347 | return $result; |
348 | } |
349 | |
350 | #endregion |
351 | |
352 | #region IDiContainer |
353 | |
354 | public function has(string $id): bool //@phpstan-ignore-line [TYPE_INTERFACE] |
355 | { |
356 | return isset($this->mapping[$id]); |
357 | } |
358 | |
359 | public function get(string $id): object //@phpstan-ignore-line [TYPE_INTERFACE] |
360 | { |
361 | $this->throwIfDisposed(); |
362 | |
363 | if (!$this->has($id)) { |
364 | throw new DiContainerNotFoundException($id); |
365 | } |
366 | |
367 | $item = $this->mapping[$id]; |
368 | |
369 | return $this->create($item, [], 0, false, [$item]); |
370 | } |
371 | |
372 | public function new(string $idOrClassName, array $arguments = []): object |
373 | { |
374 | $this->throwIfDisposed(); |
375 | |
376 | if ($this->has($idOrClassName) && empty($arguments)) { |
377 | return $this->get($idOrClassName); |
378 | } |
379 | |
380 | $item = $this->getMappingItem($idOrClassName, false); |
381 | if ($item !== null) { |
382 | if (!empty($arguments)) { |
383 | if ($item->type === DiItem::TYPE_VALUE) { |
384 | throw new DiContainerArgumentException($idOrClassName . ': DiItem::TYPE_VALUE'); |
385 | } |
386 | if ($item->lifecycle === DiItem::LIFECYCLE_SINGLETON) { |
387 | throw new DiContainerArgumentException($idOrClassName . ': DiItem::LIFECYCLE_SINGLETON'); |
388 | } |
389 | } |
390 | |
391 | return $this->create($item, $arguments, 0, false, [$item]); |
392 | } |
393 | |
394 | /** @var class-string $idOrClassName */ |
395 | return $this->createFromClassName($idOrClassName, $arguments, 0, false, [DiItem::class($idOrClassName)]); |
396 | } |
397 | |
398 | public function call(callable $callback, array $arguments = []): mixed |
399 | { |
400 | /** @var ReflectionFunctionAbstract|null */ |
401 | $reflectionFunc = null; |
402 | |
403 | if (is_string($callback)) { |
404 | $methodArray = Text::split($callback, '::'); |
405 | $methodCount = Arr::getCount($methodArray); |
406 | if ($methodCount === 0 || 2 < $methodCount) { |
407 | throw new TypeError('$callback: ' . $callback); |
408 | } |
409 | if ($methodCount === 1) { |
410 | $reflectionFunc = new ReflectionFunction($callback); |
411 | } else { |
412 | $reflectionClass = new ReflectionClass($methodArray[0]); //@phpstan-ignore-line クラスと信じるしかないやん |
413 | $reflectionFunc = $reflectionClass->getMethod($methodArray[1]); |
414 | } |
415 | } elseif (is_array($callback)) { |
416 | if (Arr::getCount($callback) !== 2) { |
417 | throw new TypeError('$callback: ' . Text::dump($callback)); |
418 | } |
419 | $reflectionClass = new ReflectionClass($callback[0]); |
420 | $reflectionFunc = $reflectionClass->getMethod($callback[1]); |
421 | } elseif (is_callable($callback)) { |
422 | $reflectionFunc = new ReflectionFunction($callback); //@phpstan-ignore-line |
423 | } |
424 | |
425 | if ($reflectionFunc === null) { //@phpstan-ignore-line |
426 | throw new TypeError('$callback: ' . Text::dump($callback)); |
427 | } |
428 | |
429 | $item = new DiItem(DiItem::LIFECYCLE_TRANSIENT, DiItem::TYPE_TYPE, $reflectionFunc->name, true); |
430 | $parameters = $this->generateParameterValues($reflectionFunc, $arguments, 0, false, [$item]); |
431 | |
432 | return call_user_func_array($callback, $parameters); |
433 | } |
434 | |
435 | public function clone(): IScopedDiContainer |
436 | { |
437 | return new ScopedDiContainer($this); |
438 | } |
439 | |
440 | #endregion |
441 | |
442 | #region DisposerBase |
443 | |
444 | protected function disposeImpl(): void |
445 | { |
446 | foreach ($this->mapping as $item) { |
447 | $item->dispose(); |
448 | } |
449 | |
450 | $this->mapping = []; |
451 | } |
452 | |
453 | #endregion |
454 | } |