Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
61 / 61 |
|
96.77% |
30 / 31 |
|
84.21% |
16 / 19 |
|
87.50% |
7 / 8 |
CRAP | |
0.00% |
0 / 1 |
| Creator | |
100.00% |
61 / 61 |
|
96.77% |
30 / 31 |
|
84.21% |
16 / 19 |
|
100.00% |
8 / 8 |
17.01 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| create | |
100.00% |
21 / 21 |
|
100.00% |
7 / 7 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| resolve | |
100.00% |
8 / 8 |
|
90.00% |
9 / 10 |
|
66.67% |
4 / 6 |
|
100.00% |
1 / 1 |
4.59 | |||
| resolveConstructor | |
100.00% |
15 / 15 |
|
100.00% |
6 / 6 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| applyCallAttributes | |
100.00% |
13 / 13 |
|
100.00% |
4 / 4 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
2.15 | |||
| getReflectionClass | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| container | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| creator | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celema\Wire; |
| 6 | |
| 7 | use Celema\Wire\Exception\WireException; |
| 8 | use Override; |
| 9 | use Psr\Container\ContainerInterface as Container; |
| 10 | use ReflectionClass; |
| 11 | use Throwable; |
| 12 | |
| 13 | /** @psalm-api */ |
| 14 | class Creator implements CreatorInterface |
| 15 | { |
| 16 | use ResolvesAbstractFunctions; |
| 17 | |
| 18 | /** @var array<class-string, ReflectionClass> */ |
| 19 | private static array $reflectionCache = []; |
| 20 | |
| 21 | public function __construct( |
| 22 | protected readonly ?Container $container = null, |
| 23 | ) {} |
| 24 | |
| 25 | /** @param class-string $class */ |
| 26 | #[Override] |
| 27 | public function create( |
| 28 | string $class, |
| 29 | array $predefinedArgs = [], |
| 30 | array $predefinedTypes = [], |
| 31 | ?callable $injectCallback = null, |
| 32 | string $constructor = '', |
| 33 | ): object { |
| 34 | if ($constructor !== '') { |
| 35 | // Factory method: wrap reflection lookup, let invocation bubble |
| 36 | try { |
| 37 | $rmethod = self::getReflectionClass($class)->getMethod($constructor); |
| 38 | } catch (Throwable $e) { |
| 39 | throw new WireException( |
| 40 | 'Unresolvable: ' . $class . '::' . $constructor . ' - ' . $e->getMessage(), |
| 41 | previous: $e, |
| 42 | ); |
| 43 | } |
| 44 | |
| 45 | $args = $this->resolveArgs( |
| 46 | $rmethod, |
| 47 | predefinedArgs: $predefinedArgs, |
| 48 | predefinedTypes: $predefinedTypes, |
| 49 | injectCallback: $injectCallback, |
| 50 | ); |
| 51 | $instance = $rmethod->invoke(null, ...$args); |
| 52 | assert(is_object($instance), 'Factory methods must return an object'); |
| 53 | } else { |
| 54 | $instance = $this->resolveConstructor( |
| 55 | $class, |
| 56 | $predefinedArgs, |
| 57 | $predefinedTypes, |
| 58 | $injectCallback, |
| 59 | ); |
| 60 | } |
| 61 | |
| 62 | return $this->applyCallAttributes($instance, $predefinedTypes, $injectCallback); |
| 63 | } |
| 64 | |
| 65 | #[Override] |
| 66 | public function resolve( |
| 67 | string $id, |
| 68 | array $predefinedTypes = [], |
| 69 | ?callable $injectCallback = null, |
| 70 | ): object { |
| 71 | if (!$this->container?->has($id)) { |
| 72 | if (!class_exists($id)) { |
| 73 | throw new WireException('Unresolvable: ' . $id . ' is neither a container entry nor a class'); |
| 74 | } |
| 75 | |
| 76 | return $this->create($id, predefinedTypes: $predefinedTypes, injectCallback: $injectCallback); |
| 77 | } |
| 78 | |
| 79 | // The container owns the entry's lifetime and configuration, including |
| 80 | // its calls, so the result is returned as is. |
| 81 | /** @psalm-suppress MixedAssignment */ |
| 82 | $instance = $this->container->get($id); |
| 83 | |
| 84 | if (!is_object($instance)) { |
| 85 | throw new WireException('Unresolvable: container entry ' . $id . ' is not an object'); |
| 86 | } |
| 87 | |
| 88 | return $instance; |
| 89 | } |
| 90 | |
| 91 | /** @param class-string $class */ |
| 92 | protected function resolveConstructor( |
| 93 | string $class, |
| 94 | array $predefinedArgs, |
| 95 | array $predefinedTypes, |
| 96 | ?callable $injectCallback, |
| 97 | ): object { |
| 98 | try { |
| 99 | $rcls = self::getReflectionClass($class); |
| 100 | } catch (Throwable $e) { |
| 101 | throw new WireException( |
| 102 | 'Unresolvable: ' . $class . ' - ' . $e->getMessage(), |
| 103 | previous: $e, |
| 104 | ); |
| 105 | } |
| 106 | |
| 107 | if (!$rcls->isInstantiable()) { |
| 108 | throw new WireException('Unresolvable: ' . $class . ' cannot be instantiated'); |
| 109 | } |
| 110 | |
| 111 | $args = new ConstructorResolver($this)->resolve( |
| 112 | $rcls, |
| 113 | predefinedArgs: $predefinedArgs, |
| 114 | predefinedTypes: $predefinedTypes, |
| 115 | injectCallback: $injectCallback, |
| 116 | ); |
| 117 | |
| 118 | return $rcls->newInstance(...$args); |
| 119 | } |
| 120 | |
| 121 | protected function applyCallAttributes( |
| 122 | object $instance, |
| 123 | array $predefinedTypes = [], |
| 124 | ?callable $injectCallback = null, |
| 125 | ): object { |
| 126 | $callAttrs = self::getReflectionClass($instance::class)->getAttributes(Call::class); |
| 127 | |
| 128 | // See if the attribute itself has one or more Call attributes. If so, |
| 129 | // resolve/autowire the arguments of the method it states and call it. |
| 130 | foreach ($callAttrs as $callAttr) { |
| 131 | $callAttr = $callAttr->newInstance(); |
| 132 | $methodToResolve = $callAttr->method; |
| 133 | |
| 134 | /** @var callable $callable */ |
| 135 | $callable = [$instance, $methodToResolve]; |
| 136 | $args = new CallableResolver($this)->resolve( |
| 137 | $callable, |
| 138 | predefinedArgs: $callAttr->args, |
| 139 | predefinedTypes: $predefinedTypes, |
| 140 | injectCallback: $injectCallback, |
| 141 | ); |
| 142 | $callable(...$args); |
| 143 | } |
| 144 | |
| 145 | return $instance; |
| 146 | } |
| 147 | |
| 148 | /** @param class-string $class */ |
| 149 | private static function getReflectionClass(string $class): ReflectionClass |
| 150 | { |
| 151 | return self::$reflectionCache[$class] ??= new ReflectionClass($class); |
| 152 | } |
| 153 | |
| 154 | #[Override] |
| 155 | public function container(): ?Container |
| 156 | { |
| 157 | return $this->container; |
| 158 | } |
| 159 | |
| 160 | #[Override] |
| 161 | public function creator(): CreatorInterface |
| 162 | { |
| 163 | return $this; |
| 164 | } |
| 165 | } |
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 22 | protected readonly ?Container $container = null, |
| 23 | ) {} |
| 122 | object $instance, |
| 123 | array $predefinedTypes = [], |
| 124 | ?callable $injectCallback = null, |
| 125 | ): object { |
| 126 | $callAttrs = self::getReflectionClass($instance::class)->getAttributes(Call::class); |
| 127 | |
| 128 | // See if the attribute itself has one or more Call attributes. If so, |
| 129 | // resolve/autowire the arguments of the method it states and call it. |
| 130 | foreach ($callAttrs as $callAttr) { |
| 130 | foreach ($callAttrs as $callAttr) { |
| 130 | foreach ($callAttrs as $callAttr) { |
| 131 | $callAttr = $callAttr->newInstance(); |
| 130 | foreach ($callAttrs as $callAttr) { |
| 131 | $callAttr = $callAttr->newInstance(); |
| 132 | $methodToResolve = $callAttr->method; |
| 133 | |
| 134 | /** @var callable $callable */ |
| 135 | $callable = [$instance, $methodToResolve]; |
| 136 | $args = new CallableResolver($this)->resolve( |
| 137 | $callable, |
| 138 | predefinedArgs: $callAttr->args, |
| 139 | predefinedTypes: $predefinedTypes, |
| 140 | injectCallback: $injectCallback, |
| 141 | ); |
| 142 | $callable(...$args); |
| 143 | } |
| 144 | |
| 145 | return $instance; |
| 146 | } |
| 157 | return $this->container; |
| 158 | } |
| 28 | string $class, |
| 29 | array $predefinedArgs = [], |
| 30 | array $predefinedTypes = [], |
| 31 | ?callable $injectCallback = null, |
| 32 | string $constructor = '', |
| 33 | ): object { |
| 34 | if ($constructor !== '') { |
| 36 | try { |
| 37 | $rmethod = self::getReflectionClass($class)->getMethod($constructor); |
| 38 | } catch (Throwable $e) { |
| 39 | throw new WireException( |
| 40 | 'Unresolvable: ' . $class . '::' . $constructor . ' - ' . $e->getMessage(), |
| 34 | if ($constructor !== '') { |
| 35 | // Factory method: wrap reflection lookup, let invocation bubble |
| 36 | try { |
| 37 | $rmethod = self::getReflectionClass($class)->getMethod($constructor); |
| 38 | } catch (Throwable $e) { |
| 39 | throw new WireException( |
| 40 | 'Unresolvable: ' . $class . '::' . $constructor . ' - ' . $e->getMessage(), |
| 41 | previous: $e, |
| 42 | ); |
| 43 | } |
| 44 | |
| 45 | $args = $this->resolveArgs( |
| 54 | $instance = $this->resolveConstructor( |
| 55 | $class, |
| 56 | $predefinedArgs, |
| 57 | $predefinedTypes, |
| 58 | $injectCallback, |
| 59 | ); |
| 60 | } |
| 61 | |
| 62 | return $this->applyCallAttributes($instance, $predefinedTypes, $injectCallback); |
| 62 | return $this->applyCallAttributes($instance, $predefinedTypes, $injectCallback); |
| 63 | } |
| 163 | return $this; |
| 164 | } |
| 149 | private static function getReflectionClass(string $class): ReflectionClass |
| 150 | { |
| 151 | return self::$reflectionCache[$class] ??= new ReflectionClass($class); |
| 152 | } |
| 67 | string $id, |
| 68 | array $predefinedTypes = [], |
| 69 | ?callable $injectCallback = null, |
| 70 | ): object { |
| 71 | if (!$this->container?->has($id)) { |
| 72 | if (!class_exists($id)) { |
| 72 | if (!class_exists($id)) { |
| 72 | if (!class_exists($id)) { |
| 72 | if (!class_exists($id)) { |
| 73 | throw new WireException('Unresolvable: ' . $id . ' is neither a container entry nor a class'); |
| 76 | return $this->create($id, predefinedTypes: $predefinedTypes, injectCallback: $injectCallback); |
| 82 | $instance = $this->container->get($id); |
| 83 | |
| 84 | if (!is_object($instance)) { |
| 85 | throw new WireException('Unresolvable: container entry ' . $id . ' is not an object'); |
| 88 | return $instance; |
| 89 | } |
| 93 | string $class, |
| 94 | array $predefinedArgs, |
| 95 | array $predefinedTypes, |
| 96 | ?callable $injectCallback, |
| 97 | ): object { |
| 98 | try { |
| 99 | $rcls = self::getReflectionClass($class); |
| 100 | } catch (Throwable $e) { |
| 101 | throw new WireException( |
| 102 | 'Unresolvable: ' . $class . ' - ' . $e->getMessage(), |
| 107 | if (!$rcls->isInstantiable()) { |
| 108 | throw new WireException('Unresolvable: ' . $class . ' cannot be instantiated'); |
| 111 | $args = new ConstructorResolver($this)->resolve( |
| 112 | $rcls, |
| 113 | predefinedArgs: $predefinedArgs, |
| 114 | predefinedTypes: $predefinedTypes, |
| 115 | injectCallback: $injectCallback, |
| 116 | ); |
| 117 | |
| 118 | return $rcls->newInstance(...$args); |
| 119 | } |