Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.64% covered (success)
94.64%
159 / 168
69.23% covered (warning)
69.23%
9 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
DiContainer
94.64% covered (success)
94.64%
159 / 168
69.23% covered (warning)
69.23%
9 / 13
74.84
0.00% covered (danger)
0.00%
0 / 1
 getMappingItem
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
6.03
 canSetValue
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
5.93
 getItemFromPropertyType
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 generateParameterValues
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
1 / 1
22
 createFromClassName
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 setMembers
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
7.05
 create
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 new
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
 call
86.36% covered (warning)
86.36%
19 / 22
0.00% covered (danger)
0.00%
0 / 1
9.21
 clone
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 disposeImpl
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core\DI;
6
7use ReflectionClass;
8use ReflectionFunction;
9use ReflectionFunctionAbstract;
10use ReflectionMethod;
11use ReflectionNamedType;
12use ReflectionParameter;
13use ReflectionProperty;
14use ReflectionType;
15use ReflectionUnionType;
16use TypeError;
17use PeServer\Core\Collection\Arr;
18use PeServer\Core\DI\DiItem;
19use PeServer\Core\DI\Inject;
20use PeServer\Core\DI\IScopedDiContainer;
21use PeServer\Core\DI\ScopedDiContainer;
22use PeServer\Core\DisposerBase;
23use PeServer\Core\IDisposable;
24use PeServer\Core\ReflectionUtility;
25use PeServer\Core\Text;
26use PeServer\Core\Throws\DiContainerArgumentException;
27use PeServer\Core\Throws\DiContainerNotFoundException;
28use PeServer\Core\Throws\DiContainerUndefinedTypeException;
29use PeServer\Core\Throws\NotImplementedException;
30use PeServer\Core\TypeUtility;
31
32/**
33 * DIコンテナ実装。
34 */
35class 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}