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}