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}