Passed
Push — main ( 92bf86...8347d6 )
by Thomas
02:44
created

Resolver::resolveCallAttributes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 17
ccs 9
cts 9
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Conia\Chuck\Registry;
6
7
use Closure;
8
use Conia\Chuck\Exception\ContainerException;
9
use Conia\Chuck\Exception\NotFoundException;
10
use ReflectionClass;
11
use ReflectionFunction;
12
use ReflectionFunctionAbstract;
13
use ReflectionMethod;
14
use ReflectionNamedType;
15
use ReflectionObject;
16
use ReflectionParameter;
17
use Throwable;
18
19
class Resolver
20
{
21 151
    public function __construct(protected readonly Registry $registry)
22
    {
23 151
    }
24
25
    /** @psalm-param class-string $class */
26 89
    public function autowire(string $class, array $predefinedArgs = []): object
27
    {
28 89
        if (!$this->registry->autowire) {
29
            try {
30 2
                $this->registry->new($class, ...$predefinedArgs);
31 1
            } catch (Throwable $e) {
32 1
                throw new ContainerException(
33 1
                    "Autowiring is turned off. Tried to instantiate class '{$class}'"
34 1
                );
35
            }
36
        }
37
38 88
        $rc = new ReflectionClass($class);
39 88
        $args = $this->resolveConstructorArgs($rc, $predefinedArgs);
40
41
        try {
42 85
            return $this->resolveCallAttributes($rc->newInstance(...$args));
43 2
        } catch (Throwable $e) {
44 2
            throw new ContainerException(
45 2
                'Autowiring unresolvable: ' . $class . ' Details: ' . $e->getMessage()
46 2
            );
47
        }
48
    }
49
50 92
    public function resolveCallAttributes(object $instance): object
51
    {
52 92
        $callAttrs = (new ReflectionObject($instance))->getAttributes(Call::class);
53
54
        // See if the attribute itself has one or more Call attributes. If so,
55
        // resolve/autowire the arguments of the method it states and call it.
56 92
        foreach ($callAttrs as $callAttr) {
57 3
            $callAttr = $callAttr->newInstance();
58 3
            $methodToResolve = $callAttr->method;
59
60
            /** @psalm-var callable */
61 3
            $callable = [$instance, $methodToResolve];
62 3
            $args = $this->resolveCallableArgs($callable, $callAttr->args);
63 3
            $callable(...$args);
64
        }
65
66 92
        return $instance;
67
    }
68
69 36
    public function resolveParam(ReflectionParameter $param): mixed
70
    {
71 36
        $type = $param->getType();
72
73 36
        if ($type instanceof ReflectionNamedType) {
74
            try {
75 32
                return $this->registry->get(ltrim($type->getName(), '?'));
76 6
            } catch (NotFoundException $e) {
77 5
                if ($param->isDefaultValueAvailable()) {
78 4
                    return $param->getDefaultValue();
79
                }
80
81 1
                throw $e;
82
            }
83
        } else {
84 4
            if ($type) {
85 2
                throw new ContainerException(
86 2
                    "Autowiring does not support union or intersection types. Source: \n" .
87 2
                        $this->getParamInfo($param)
88 2
                );
89
            }
90
91 2
            throw new ContainerException(
92 2
                "Autowired entities need to have typed constructor parameters. Source: \n" .
93 2
                    $this->getParamInfo($param)
94 2
            );
95
        }
96
    }
97
98 6
    public function getParamInfo(ReflectionParameter $param): string
99
    {
100 6
        $type = $param->getType();
101 6
        $rf = $param->getDeclaringFunction();
102 6
        $rc = null;
103
104 6
        if ($rf instanceof ReflectionMethod) {
105 6
            $rc = $rf->getDeclaringClass();
106
        }
107
108 6
        return ($rc ? $rc->getName() . '::' : '') .
109 6
            ($rf->getName() . '(..., ') .
110 6
            ($type ? (string)$type . ' ' : '') .
111 6
            '$' . $param->getName() . ', ...)';
112
    }
113
114
    /** @psalm-param callable-array|callable $callable */
115 15
    public function resolveCallableArgs(array|callable $callable, array $predefinedArgs = []): array
116
    {
117 15
        $callable = Closure::fromCallable($callable);
118 15
        $rf = new ReflectionFunction($callable);
119 15
        $predefinedArgs = array_merge($this->getInjectedArgs($rf), $predefinedArgs);
120
121 15
        return $this->resolveArgs($rf, $predefinedArgs);
122
    }
123
124
    /** @psalm-param ReflectionClass|class-string $class */
125 88
    public function resolveConstructorArgs(ReflectionClass|string $class, array $predefinedArgs = []): array
126
    {
127 88
        $rc = is_string($class) ? new ReflectionClass($class) : $class;
128 88
        $constructor = $rc->getConstructor();
129
130 88
        if ($constructor) {
131 79
            $combinedArgs = array_merge($this->getInjectedArgs($constructor), $predefinedArgs);
132
133 79
            return $this->resolveArgs($constructor, $combinedArgs);
134
        }
135
136 19
        return $predefinedArgs;
137
    }
138
139 87
    protected function resolveArgs(
140
        ?ReflectionFunctionAbstract $rf,
141
        array $predefinedArgs = [],
142
    ): array {
143 87
        $args = [];
144
145 87
        if ($rf) {
146 87
            foreach ($rf->getParameters() as $param) {
147 22
                $name = $param->getName();
148
149 22
                if (isset($predefinedArgs[$name])) {
150
                    /** @psalm-var list<mixed> */
151 8
                    $args[] = $predefinedArgs[$name];
152
                } else {
153
                    /** @psalm-var list<mixed> */
154 21
                    $args[] = $this->resolveParam($param);
155
                }
156
            }
157
        }
158
159 83
        return $args;
160
    }
161
162 87
    protected function getInjectedArgs(ReflectionFunctionAbstract $rf): array
163
    {
164
        /** @psalm-var array<non-empty-string, mixed> */
165 87
        $result = [];
166 87
        $injectAttrs = $rf->getAttributes(Inject::class);
167
168 87
        foreach ($injectAttrs as $injectAttr) {
169 3
            $instance = $injectAttr->newInstance();
170
171
            /** @psalm-suppress MixedAssignment */
172 3
            foreach ($instance->args as $name => $value) {
173 3
                assert(is_string($name));
174
175 3
                if (is_string($value)) {
176 3
                    if ($this->registry->has($value)) {
177 3
                        $result[$name] = $this->registry->get($value);
178 3
                    } elseif (class_exists($value)) {
179 2
                        $result[$name] = $this->autowire($value);
180
                    } else {
181 3
                        $result[$name] = $value;
182
                    }
183
                } else {
184 2
                    $result[$name] = $value;
185
                }
186
            }
187
        }
188
189 87
        return $result;
190
    }
191
}
192