Passed
Pull Request — master (#862)
by Aleksei
06:29
created

Factory::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 10
ccs 8
cts 8
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Core\Internal;
6
7
use Psr\Container\ContainerInterface;
8
use Spiral\Core\BinderInterface;
9
use Spiral\Core\Container\Autowire;
10
use Spiral\Core\Container\InjectorInterface;
11
use Spiral\Core\Container\SingletonInterface;
12
use Spiral\Core\Exception\Container\AutowireException;
13
use Spiral\Core\Exception\Container\ContainerException;
14
use Spiral\Core\Exception\Container\InjectionException;
15
use Spiral\Core\Exception\Container\NotCallableException;
16
use Spiral\Core\Exception\Container\NotFoundException;
17
use Spiral\Core\Exception\Resolver\ValidationException;
18
use Spiral\Core\Exception\Resolver\WrongTypeException;
19
use Spiral\Core\FactoryInterface;
20
use Spiral\Core\InvokerInterface;
21
use Spiral\Core\ResolverInterface;
22
use WeakReference;
23
24
/**
25
 * @internal
26
 */
27
final class Factory implements FactoryInterface
28
{
29
    use DestructorTrait;
30
31
    private State $state;
32
    private BinderInterface $binder;
33
    private InvokerInterface $invoker;
34
    private ContainerInterface $container;
35
    private ResolverInterface $resolver;
36
    private Tracer $tracer;
37
38 990
    public function __construct(Registry $constructor)
39
    {
40 990
        $constructor->set('factory', $this);
41
42 990
        $this->state = $constructor->get('state', State::class);
43 990
        $this->binder = $constructor->get('binder', BinderInterface::class);
44 990
        $this->invoker = $constructor->get('invoker', InvokerInterface::class);
45 990
        $this->container = $constructor->get('container', ContainerInterface::class);
46 990
        $this->resolver = $constructor->get('resolver', ResolverInterface::class);
47 990
        $this->tracer = $constructor->get('tracer', Tracer::class);
48
    }
49
50
    /**
51
     * @param string|null $context Related to parameter caused injection if any.
52
     *
53
     * @throws \Throwable
54
     */
55 829
    public function make(string $alias, array $parameters = [], string $context = null): mixed
56
    {
57 829
        if (!isset($this->state->bindings[$alias])) {
58 674
            $this->tracer->push(false, action: 'autowire', alias: $alias, context: $context);
0 ignored issues
show
Bug introduced by
'autowire' of type string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

58
            $this->tracer->push(false, /** @scrutinizer ignore-type */ action: 'autowire', alias: $alias, context: $context);
Loading history...
Bug introduced by
$context of type null|string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

58
            $this->tracer->push(false, action: 'autowire', alias: $alias, /** @scrutinizer ignore-type */ context: $context);
Loading history...
59
            try {
60
                //No direct instructions how to construct class, make is automatically
61 674
                return $this->autowire($alias, $parameters, $context);
62
            } finally {
63 674
                $this->tracer->pop(false);
64
            }
65
        }
66
67 751
        $avoidCache = $parameters !== [];
68 751
        $binding = $this->state->bindings[$alias];
69
        try {
70 751
            $this->tracer->push(
71 751
                false,
72 751
                action: 'resolve from binding',
73 751
                alias: $alias,
74 751
                context: $context,
75 751
                binding: $binding,
76 751
            );
77 751
            $this->tracer->push(true);
78
79 751
            if (\is_object($binding)) {
80 718
                if ($binding::class === WeakReference::class) {
81 512
                    if (($avoidCache || $binding->get() === null) && \class_exists($alias)) {
82
                        try {
83 3
                            $this->tracer->push(false, alias: $alias, source: WeakReference::class, context: $context);
84 3
                            $object = $this->createInstance($alias, $parameters, $context);
85 3
                            if ($avoidCache) {
86 1
                                return $object;
87
                            }
88 2
                            $binding = $this->state->bindings[$alias] = WeakReference::create($object);
89
                        } catch (\Throwable) {
90
                            throw new ContainerException($this->tracer->combineTraceMessage(\sprintf(
91
                                'Can\'t resolve `%s`: can\'t instantiate `%s` from WeakReference binding.',
92
                                $this->tracer->getRootAlias(),
93
                                $alias,
94
                            )));
95
                        } finally {
96 3
                            $this->tracer->pop();
97
                        }
98
                    }
99 512
                    return $binding->get();
100
                }
101
                //When binding is instance, assuming singleton
102 645
                return $avoidCache
103 3
                    ? $this->createInstance($binding::class, $parameters, $context)
104 645
                    : $binding;
105
            }
106
107 590
            if (\is_string($binding)) {
108
                //Binding is pointing to something else
109 548
                return $this->make($binding, $parameters, $context);
110
            }
111
112 377
            unset($this->state->bindings[$alias]);
113
            try {
114 377
                $instance = $binding[0] === $alias
115 313
                    ? $this->autowire($alias, $parameters, $context)
116 370
                    : $this->evaluateBinding($alias, $binding[0], $parameters, $context);
117
            } finally {
118
                /** @psalm-var class-string $alias */
119 377
                $this->state->bindings[$alias] = $binding;
120
            }
121
        } finally {
122 751
            $this->tracer->pop(true);
123 751
            $this->tracer->pop(false);
124
        }
125
126 372
        if ($binding[1]) {
127
            // Indicates singleton
128
            /** @psalm-var class-string $alias */
129 363
            $this->state->bindings[$alias] = $instance;
130
        }
131
132 372
        return $instance;
133
    }
134
135
    /**
136
     * Automatically create class.
137
     * Object will be cached if the $arguments list is empty.
138
     *
139
     * @param class-string $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
140
     *
141
     * @throws AutowireException
142
     * @throws \Throwable
143
     */
144 675
    private function autowire(string $class, array $arguments, string $context = null): object
145
    {
146 675
        if (!(\class_exists($class) || (
147 674
            \interface_exists($class)
148 674
                &&
149 674
                (isset($this->state->injectors[$class]) || $this->binder->hasInjector($class))
150
        ))
151
        ) {
152 439
            throw new NotFoundException($this->tracer->combineTraceMessage(\sprintf(
153 439
                'Can\'t resolve `%s`: undefined class or binding `%s`.',
154 439
                $this->tracer->getRootAlias(),
155 439
                $class
156 439
            )));
157
        }
158
159
        // automatically create instance
160 663
        $instance = $this->createInstance($class, $arguments, $context);
161
162
        // apply registration functions to created instance
163 636
        return $arguments === []
164 627
            ? $this->registerInstance($instance)
165 636
            : $instance;
166
    }
167
168
    /**
169
     * @param mixed $target Value that was bound by user.
170
     *
171
     * @throws ContainerException
172
     * @throws \Throwable
173
     */
174 370
    private function evaluateBinding(
175
        string $alias,
176
        mixed $target,
177
        array $parameters,
178
        string $context = null
179
    ): mixed {
180 370
        if (\is_string($target)) {
181
            // Reference
182 345
            return $this->make($target, $parameters, $context);
183
        }
184
185 336
        if ($target instanceof Autowire) {
186 3
            return $target->resolve($this, $parameters);
187
        }
188
189
        try {
190 333
            return $this->invoker->invoke($target, $parameters);
191 8
        } catch (NotCallableException $e) {
192 2
            throw new ContainerException(
193 2
                $this->tracer->combineTraceMessage(\sprintf('Invalid binding for `%s`.', $alias)),
194 2
                $e->getCode(),
195 2
                $e,
196 2
            );
197
        }
198
    }
199
200
    /**
201
     * Create instance of desired class.
202
     *
203
     * @template TObject
204
     *
205
     * @param class-string<TObject> $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<TObject> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<TObject>.
Loading history...
206
     * @param array $parameters Constructor parameters.
207
     *
208
     * @return TObject
209
     *
210
     * @throws ContainerException
211
     * @throws \Throwable
212
     */
213 667
    private function createInstance(string $class, array $parameters, string $context = null): object
214
    {
215
        try {
216 667
            $reflection = new \ReflectionClass($class);
217
        } catch (\ReflectionException $e) {
218
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
219
        }
220
221
        //We have to construct class using external injector when we know exact context
222 667
        if ($parameters === [] && $this->binder->hasInjector($class)) {
223 343
            $injector = $this->state->injectors[$reflection->getName()];
224
225
            try {
226 343
                $injectorInstance = $this->container->get($injector);
227
228 335
                if (!$injectorInstance instanceof InjectorInterface) {
229 2
                    throw new InjectionException(
230 2
                        \sprintf(
231 2
                            "Class '%s' must be an instance of InjectorInterface for '%s'.",
232 2
                            $injectorInstance::class,
233 2
                            $reflection->getName()
234 2
                        )
235 2
                    );
236
                }
237
238
                /** @var InjectorInterface<TObject> $injectorInstance */
239 333
                $instance = $injectorInstance->createInjection($reflection, $context);
240 331
                if (!$reflection->isInstance($instance)) {
241 1
                    throw new InjectionException(
242 1
                        \sprintf(
243 1
                            "Invalid injection response for '%s'.",
244 1
                            $reflection->getName()
245 1
                        )
246 1
                    );
247
                }
248
249 330
                return $instance;
250
            } finally {
251 343
                $this->state->injectors[$reflection->getName()] = $injector;
252
            }
253
        }
254
255 655
        if (!$reflection->isInstantiable()) {
256 4
            $itIs = match (true) {
257 4
                $reflection->isEnum() => 'Enum',
0 ignored issues
show
Bug introduced by
The method isEnum() does not exist on ReflectionClass. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

257
                $reflection->/** @scrutinizer ignore-call */ 
258
                             isEnum() => 'Enum',

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
258 4
                $reflection->isAbstract() => 'Abstract class',
259 4
                default => 'Class',
260 4
            };
261 4
            throw new ContainerException(
262 4
                $this->tracer->combineTraceMessage(\sprintf('%s `%s` can not be constructed.', $itIs, $class)),
263 4
            );
264
        }
265
266 654
        $constructor = $reflection->getConstructor();
267
268 654
        if ($constructor !== null) {
269
            try {
270 596
                $this->tracer->push(false, action: 'resolve arguments', signature: $constructor);
0 ignored issues
show
Bug introduced by
$constructor of type ReflectionMethod is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

270
                $this->tracer->push(false, action: 'resolve arguments', /** @scrutinizer ignore-type */ signature: $constructor);
Loading history...
Bug introduced by
'resolve arguments' of type string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

270
                $this->tracer->push(false, /** @scrutinizer ignore-type */ action: 'resolve arguments', signature: $constructor);
Loading history...
271 596
                $this->tracer->push(true);
272 596
                $arguments = $this->resolver->resolveArguments($constructor, $parameters);
273 20
            } catch (ValidationException $e) {
274 6
                throw new ContainerException(
275 6
                    $this->tracer->combineTraceMessage(
276 6
                        \sprintf(
277 6
                            'Can\'t resolve `%s`. %s',
278 6
                            $this->tracer->getRootAlias(),
279 6
                            $e->getMessage()
280 6
                        )
281 6
                    ),
282 6
                );
283
            } finally {
284 596
                $this->tracer->pop(true);
285 596
                $this->tracer->pop(false);
286
            }
287
            try {
288
                // Using constructor with resolved arguments
289 576
                $this->tracer->push(false, call: "$class::__construct", arguments: $arguments);
290 576
                $this->tracer->push(true);
291 576
                $instance = new $class(...$arguments);
292 1
            } catch (\TypeError $e) {
293
                throw new WrongTypeException($constructor, $e);
294
            } finally {
295 576
                $this->tracer->pop(true);
296 576
                $this->tracer->pop(false);
297
            }
298
        } else {
299
            // No constructor specified
300 510
            $instance = $reflection->newInstance();
301
        }
302
303 633
        return $instance;
304
    }
305
306
    /**
307
     * Register instance in container, might perform methods like auto-singletons, log populations
308
     * and etc.
309
     *
310
     * @template TObject of object
311
     *
312
     * @param TObject $instance Created object.
0 ignored issues
show
Bug introduced by
The type Spiral\Core\Internal\TObject was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
313
     *
314
     * @return TObject
315
     */
316 627
    private function registerInstance(object $instance): object
317
    {
318
        //Declarative singletons
319 627
        if ($instance instanceof SingletonInterface) {
320 372
            $alias = $instance::class;
321 372
            if (!isset($this->state->bindings[$alias])) {
322 372
                $this->state->bindings[$alias] = $instance;
323
            }
324
        }
325
326 627
        return $instance;
327
    }
328
}
329