Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
61 / 61
96.77% covered (success)
96.77%
30 / 31
84.21% covered (warning)
84.21%
16 / 19
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Creator
100.00% covered (success)
100.00%
61 / 61
96.77% covered (success)
96.77%
30 / 31
84.21% covered (warning)
84.21%
16 / 19
100.00% covered (success)
100.00%
8 / 8
17.01
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 resolve
100.00% covered (success)
100.00%
8 / 8
90.00% covered (success)
90.00%
9 / 10
66.67% covered (warning)
66.67%
4 / 6
100.00% covered (success)
100.00%
1 / 1
4.59
 resolveConstructor
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 applyCallAttributes
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
4 / 4
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
2.15
 getReflectionClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 container
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 creator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Wire;
6
7use Celema\Wire\Exception\WireException;
8use Override;
9use Psr\Container\ContainerInterface as Container;
10use ReflectionClass;
11use Throwable;
12
13/** @psalm-api */
14class 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}

Branches

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.

Creator->__construct
22        protected readonly ?Container $container = null,
23    ) {}
Creator->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) {
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    }
Creator->container
157        return $this->container;
158    }
Creator->create
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    }
Creator->creator
163        return $this;
164    }
Creator->getReflectionClass
149    private static function getReflectionClass(string $class): ReflectionClass
150    {
151        return self::$reflectionCache[$class] ??= new ReflectionClass($class);
152    }
Creator->resolve
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    }
Creator->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(),
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    }