Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
68 / 68
97.22% covered (success)
97.22%
35 / 36
34.09% covered (danger)
34.09%
15 / 44
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Creator
100.00% covered (success)
100.00%
68 / 68
97.22% covered (success)
97.22%
35 / 36
34.09% covered (danger)
34.09%
15 / 44
100.00% covered (success)
100.00%
7 / 7
99.74
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%
38 / 38
95.83% covered (success)
95.83%
23 / 24
20.00% covered (danger)
20.00%
7 / 35
100.00% covered (success)
100.00%
1 / 1
50.47
 resolveConstructor
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 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 Celemas\Wire;
6
7use Celemas\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|WireContainer|null $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        $createdByWire = true;
35
36        if ($constructor !== '') {
37            // Factory method: wrap reflection lookup, let invocation bubble
38            try {
39                $rmethod = self::getReflectionClass($class)->getMethod($constructor);
40            } catch (Throwable $e) {
41                throw new WireException(
42                    'Unresolvable: ' . $class . '::' . $constructor . ' - ' . $e->getMessage(),
43                    previous: $e,
44                );
45            }
46
47            $args = $this->resolveArgs(
48                $rmethod,
49                predefinedArgs: $predefinedArgs,
50                predefinedTypes: $predefinedTypes,
51                injectCallback: $injectCallback,
52            );
53            $instance = $rmethod->invoke(null, ...$args);
54        } elseif ($this->container && $this->container->has($class)) {
55            if (is_a($this->container, WireContainer::class)) {
56                /** @psalm-suppress MixedAssignment */
57                $value = $this->container->definition($class);
58
59                if (is_string($value) && class_exists($value)) {
60                    $instance = $this->resolveConstructor(
61                        $value,
62                        $predefinedArgs,
63                        $predefinedTypes,
64                        $injectCallback,
65                    );
66                } else {
67                    $createdByWire = false;
68                    /** @psalm-suppress MixedAssignment */
69                    $instance = $this->container->get($class);
70                }
71            } else {
72                $createdByWire = false;
73                /** @psalm-suppress MixedAssignment */
74                $instance = $this->container->get($class);
75            }
76        } else {
77            $instance = $this->resolveConstructor(
78                $class,
79                $predefinedArgs,
80                $predefinedTypes,
81                $injectCallback,
82            );
83        }
84
85        assert(is_object($instance), 'Created instance must be an object');
86
87        if (!$createdByWire) {
88            return $instance;
89        }
90
91        return $this->applyCallAttributes($instance, $predefinedTypes, $injectCallback);
92    }
93
94    /** @param class-string $class */
95    protected function resolveConstructor(
96        string $class,
97        array $predefinedArgs,
98        array $predefinedTypes,
99        ?callable $injectCallback,
100    ): object {
101        try {
102            $rcls = self::getReflectionClass($class);
103        } catch (Throwable $e) {
104            throw new WireException(
105                'Unresolvable: ' . $class . ' - ' . $e->getMessage(),
106                previous: $e,
107            );
108        }
109
110        $args = new ConstructorResolver($this)->resolve(
111            $rcls,
112            predefinedArgs: $predefinedArgs,
113            predefinedTypes: $predefinedTypes,
114            injectCallback: $injectCallback,
115        );
116
117        return $rcls->newInstance(...$args);
118    }
119
120    protected function applyCallAttributes(
121        object $instance,
122        array $predefinedTypes = [],
123        ?callable $injectCallback = null,
124    ): object {
125        $callAttrs = self::getReflectionClass($instance::class)->getAttributes(Call::class);
126
127        // See if the attribute itself has one or more Call attributes. If so,
128        // resolve/autowire the arguments of the method it states and call it.
129        foreach ($callAttrs as $callAttr) {
130            $callAttr = $callAttr->newInstance();
131            $methodToResolve = $callAttr->method;
132
133            /** @var callable $callable */
134            $callable = [$instance, $methodToResolve];
135            $args = new CallableResolver($this)->resolve(
136                $callable,
137                predefinedArgs: $callAttr->args,
138                predefinedTypes: $predefinedTypes,
139                injectCallback: $injectCallback,
140            );
141            $callable(...$args);
142        }
143
144        return $instance;
145    }
146
147    /** @param class-string $class */
148    private static function getReflectionClass(string $class): ReflectionClass
149    {
150        return self::$reflectionCache[$class] ??= new ReflectionClass($class);
151    }
152
153    #[Override]
154    public function container(): ?Container
155    {
156        return $this->container;
157    }
158
159    #[Override]
160    public function creator(): CreatorInterface
161    {
162        return $this;
163    }
164}

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|WireContainer|null $container = null,
23    ) {}
Creator->applyCallAttributes
121        object $instance,
122        array $predefinedTypes = [],
123        ?callable $injectCallback = null,
124    ): object {
125        $callAttrs = self::getReflectionClass($instance::class)->getAttributes(Call::class);
126
127        // See if the attribute itself has one or more Call attributes. If so,
128        // resolve/autowire the arguments of the method it states and call it.
129        foreach ($callAttrs as $callAttr) {
129        foreach ($callAttrs as $callAttr) {
129        foreach ($callAttrs as $callAttr) {
130            $callAttr = $callAttr->newInstance();
129        foreach ($callAttrs as $callAttr) {
130            $callAttr = $callAttr->newInstance();
131            $methodToResolve = $callAttr->method;
132
133            /** @var callable $callable */
134            $callable = [$instance, $methodToResolve];
135            $args = new CallableResolver($this)->resolve(
136                $callable,
137                predefinedArgs: $callAttr->args,
138                predefinedTypes: $predefinedTypes,
139                injectCallback: $injectCallback,
140            );
141            $callable(...$args);
142        }
143
144        return $instance;
145    }
Creator->container
156        return $this->container;
157    }
Creator->create
28        string $class,
29        array $predefinedArgs = [],
30        array $predefinedTypes = [],
31        ?callable $injectCallback = null,
32        string $constructor = '',
33    ): object {
34        $createdByWire = true;
35
36        if ($constructor !== '') {
38            try {
39                $rmethod = self::getReflectionClass($class)->getMethod($constructor);
40            } catch (Throwable $e) {
41                throw new WireException(
42                    'Unresolvable: ' . $class . '::' . $constructor . ' - ' . $e->getMessage(),
36        if ($constructor !== '') {
37            // Factory method: wrap reflection lookup, let invocation bubble
38            try {
39                $rmethod = self::getReflectionClass($class)->getMethod($constructor);
40            } catch (Throwable $e) {
41                throw new WireException(
42                    'Unresolvable: ' . $class . '::' . $constructor . ' - ' . $e->getMessage(),
43                    previous: $e,
44                );
45            }
46
47            $args = $this->resolveArgs(
54        } elseif ($this->container && $this->container->has($class)) {
54        } elseif ($this->container && $this->container->has($class)) {
54        } elseif ($this->container && $this->container->has($class)) {
55            if (is_a($this->container, WireContainer::class)) {
57                $value = $this->container->definition($class);
58
59                if (is_string($value) && class_exists($value)) {
59                if (is_string($value) && class_exists($value)) {
59                if (is_string($value) && class_exists($value)) {
59                if (is_string($value) && class_exists($value)) {
59                if (is_string($value) && class_exists($value)) {
59                if (is_string($value) && class_exists($value)) {
59                if (is_string($value) && class_exists($value)) {
60                    $instance = $this->resolveConstructor(
55            if (is_a($this->container, WireContainer::class)) {
56                /** @psalm-suppress MixedAssignment */
57                $value = $this->container->definition($class);
58
59                if (is_string($value) && class_exists($value)) {
60                    $instance = $this->resolveConstructor(
61                        $value,
62                        $predefinedArgs,
63                        $predefinedTypes,
64                        $injectCallback,
65                    );
66                } else {
67                    $createdByWire = false;
55            if (is_a($this->container, WireContainer::class)) {
54        } elseif ($this->container && $this->container->has($class)) {
55            if (is_a($this->container, WireContainer::class)) {
56                /** @psalm-suppress MixedAssignment */
57                $value = $this->container->definition($class);
58
59                if (is_string($value) && class_exists($value)) {
60                    $instance = $this->resolveConstructor(
61                        $value,
62                        $predefinedArgs,
63                        $predefinedTypes,
64                        $injectCallback,
65                    );
66                } else {
67                    $createdByWire = false;
68                    /** @psalm-suppress MixedAssignment */
69                    $instance = $this->container->get($class);
70                }
71            } else {
72                $createdByWire = false;
54        } elseif ($this->container && $this->container->has($class)) {
77            $instance = $this->resolveConstructor(
78                $class,
79                $predefinedArgs,
80                $predefinedTypes,
81                $injectCallback,
82            );
83        }
84
85        assert(is_object($instance), 'Created instance must be an object');
85        assert(is_object($instance), 'Created instance must be an object');
86
87        if (!$createdByWire) {
88            return $instance;
91        return $this->applyCallAttributes($instance, $predefinedTypes, $injectCallback);
92    }
Creator->creator
162        return $this;
163    }
Creator->getReflectionClass
148    private static function getReflectionClass(string $class): ReflectionClass
149    {
150        return self::$reflectionCache[$class] ??= new ReflectionClass($class);
151    }
Creator->resolveConstructor
96        string $class,
97        array $predefinedArgs,
98        array $predefinedTypes,
99        ?callable $injectCallback,
100    ): object {
101        try {
102            $rcls = self::getReflectionClass($class);
103        } catch (Throwable $e) {
104            throw new WireException(
105                'Unresolvable: ' . $class . ' - ' . $e->getMessage(),
110        $args = new ConstructorResolver($this)->resolve(
111            $rcls,
112            predefinedArgs: $predefinedArgs,
113            predefinedTypes: $predefinedTypes,
114            injectCallback: $injectCallback,
115        );
116
117        return $rcls->newInstance(...$args);
118    }