Passed
Push — master ( 96803c...b18ae0 )
by Divine Niiquaye
01:37
created

Container::hasMethodBinding()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Biurad opensource projects.
7
 *
8
 * PHP version 7.2 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Biurad\DependencyInjection;
19
20
use Biurad\DependencyInjection\Exceptions\ContainerResolutionException;
21
use Biurad\DependencyInjection\Exceptions\NotFoundServiceException;
22
use Closure;
23
use Nette\DI\Container as NetteContainer;
24
use Nette\DI\Helpers;
25
use Nette\DI\MissingServiceException;
26
use Nette\DI\Resolver;
27
use Nette\UnexpectedValueException;
28
use Nette\Utils\Callback;
29
use Nette\Utils\Reflection;
30
use Nette\Utils\Validators;
31
use ReflectionClass;
32
use ReflectionException;
33
use ReflectionFunction;
34
use ReflectionFunctionAbstract;
35
use ReflectionMethod;
36
use ReflectionType;
37
use Throwable;
38
39
/**
40
 * The dependency injection container default implementation.
41
 *
42
 * Auto-wiring container: declarative singletons, contextual injections, outer delegation and
43
 * ability to lazy wire.
44
 *
45
 * Container does not support setter injections, private properties and etc. Normally it will work
46
 * with classes only to be as much invisible as possible.
47
 *
48
 * @author Divine Niiquaye Ibok <[email protected]>
49
 */
50
class Container extends NetteContainer implements FactoryInterface
51
{
52
    /** @var object[] service name => instance */
53
    private $instances = [];
54
55
    /** @var array circular reference detector */
56
    private $creating;
57
58
    /** @var array */
59
    private $methods;
60
61
    /**
62
     * Provide psr container interface in order to proxy get and has requests.
63
     *
64
     * @param array $params
65
     */
66
    public function __construct(array $params = [])
67
    {
68
        $this->parameters = $params;
69
        $this->methods    = $this->getServiceMethods(\get_class_methods($this));
70
71
        parent::__construct($params);
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function has($id)
78
    {
79
        return $this->hasService($id);
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     *
85
     * @return object
86
     */
87
    public function get($id)
88
    {
89
        try {
90
            return $this->make($id);
91
        } catch (Throwable $e) {
92
            throw new NotFoundServiceException(\sprintf('Service [%s] is not found in container', $id), 0, $e);
93
        }
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99
    public function getParameter(string $name)
100
    {
101
        return Builder::arrayGet($this->parameters, $name);
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public function addService(string $name, $service)
108
    {
109
        $service = null === $service ? $name : $service;
110
        $name    = $this->aliases[$name] ?? $name;
111
112
        // Create an instancer
113
        if (\is_string($service) && \class_exists($service)) {
114
            $service = $this->createInstance($service);
115
        }
116
117
        // Report exception if name already exists.
118
        if (isset($this->instances[$name])) {
119
            throw new ContainerResolutionException("Service [$name] already exists.");
120
        }
121
122
        if (!\is_object($service)) {
123
            throw new ContainerResolutionException(
124
                \sprintf("Service '%s' must be a object, %s given.", $name, \gettype($name))
125
            );
126
        }
127
128
        // Resolving the closure of the service to return it's type hint or class.
129
        $type = $this->parseServiceType($service);
130
131
        // Resolving wiring so we could call the service parent classes and interfaces.
132
        if (!$service instanceof Closure) {
133
            $this->resolveWiring($name, $type);
134
        }
135
136
        // Resolving the method calls.
137
        $this->resolveMethod($name, self::getMethodName($name), $type);
138
139
        if ($service instanceof Closure) {
140
            // Get the method binding for the given method.
141
            $this->methods[self::getMethodName($name)] = $service;
142
            $this->types[$name] = $type;
143
        } else {
144
            $this->instances[$name] = $service;
145
        }
146
147
        return $this;
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153
    public function removeService(string $name): void
154
    {
155
        $name = $this->aliases[$name] ?? $name;
156
        unset($this->instances[$name]);
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function getService(string $name)
163
    {
164
        if (!isset($this->instances[$name])) {
165
            if (isset($this->aliases[$name])) {
166
                return $this->getService($this->aliases[$name]);
167
            }
168
            $this->instances[$name] = $this->createService($name);
169
        }
170
171
        return $this->instances[$name];
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177
    public function getServiceType(string $name): string
178
    {
179
        $method = self::getMethodName($name);
180
181
        if (isset($this->aliases[$name])) {
182
            return $this->getServiceType($this->aliases[$name]);
183
        }
184
185
        if (isset($this->types[$name])) {
186
            return $this->types[$name];
187
        }
188
189
        if ($this->hasMethodBinding($method)) {
190
            /** @var ReflectionMethod $type */
191
            $type = $this->parseBindMethod([$this, $method]);
192
193
            return $type ? $type->getName() : '';
0 ignored issues
show
introduced by
$type is of type ReflectionMethod, thus it always evaluated to true.
Loading history...
194
        }
195
196
        throw new MissingServiceException("Service '$name' not found.");
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202
    public function hasService(string $name): bool
203
    {
204
        $name = $this->aliases[$name] ?? $name;
205
206
        return $this->hasMethodBinding(self::getMethodName($name)) || isset($this->instances[$name]);
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212
    public function isCreated(string $name): bool
213
    {
214
        if (!$this->hasService($name)) {
215
            throw new MissingServiceException("Service '$name' not found.");
216
        }
217
        $name = $this->aliases[$name] ?? $name;
218
219
        return isset($this->instances[$name]);
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225
    public function createService(string $name, array $args = [])
226
    {
227
        $name   = $this->aliases[$name] ?? $name;
228
        $method = self::getMethodName($name);
229
        $cb     = $this->methods[$method] ?? null;
230
231
        if (isset($this->creating[$name])) {
232
            throw new ContainerResolutionException(
233
                \sprintf(
234
                    'Circular reference detected for services: %s.',
235
                    \implode(', ', \array_keys($this->creating))
236
                )
237
            );
238
        }
239
240
        if ($cb === null) {
241
            throw new MissingServiceException("Service '$name' not found.");
242
        }
243
244
        try {
245
            $this->creating[$name] = true;
246
            $service               = $cb instanceof Closure ? $cb(...$args) : $this->$method(...$args);
247
        } finally {
248
            unset($this->creating[$name]);
249
        }
250
251
        if (!\is_object($service)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $service does not seem to be defined for all execution paths leading up to this point.
Loading history...
252
            throw new UnexpectedValueException(
253
                "Unable to create service '$name', value returned by " .
254
                ($cb instanceof Closure ? 'closure' : "method $method()") . ' is not object.'
255
            );
256
        }
257
258
        return $service;
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     */
264
    public function getByType(string $type, bool $throw = true)
265
    {
266
        $type = Helpers::normalizeClass($type);
267
268
        if (!empty($this->wiring[$type][0])) {
269
            if (\count($names = $this->wiring[$type][0]) === 1) {
270
                return $this->getService($names[0]);
271
            }
272
            \natsort($names);
273
274
            throw new MissingServiceException(
275
                "Multiple services of type $type found: " . \implode(', ', $names) . '.'
276
            );
277
        }
278
279
        if ($throw) {
280
            if (!\class_exists($type) && !\interface_exists($type)) {
281
                throw new MissingServiceException(
282
                    "Service of type '$type' not found. Check class name because it cannot be found."
283
                );
284
            }
285
286
            foreach ($this->methods as $method => $foo) {
287
                $methodType = $this->parseBindMethod([\get_class($this), $method])->getName();
288
289
                if (\is_a($methodType, $type, true)) {
290
                    throw new MissingServiceException(
291
                        "Service of type $type is not autowired or is missing in di › export › types."
292
                    );
293
                }
294
            }
295
296
            throw new MissingServiceException(
297
                "Service of type $type not found. Did you add it to configuration file?"
298
            );
299
        }
300
301
        return null;
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     */
307
    public function createInstance(string $class, array $args = [])
308
    {
309
        try {
310
            $reflector = new ReflectionClass($class);
311
        } catch (ReflectionException $e) {
312
            throw new ContainerResolutionException("Targeted class [$class] does not exist.", 0, $e);
313
        }
314
315
        // If the type is not instantiable, the developer is attempting to resolve
316
        // an abstract type such as an Interface or Abstract Class and there is
317
        // no binding registered for the abstractions so we need to bail out.
318
        if (!$reflector->isInstantiable()) {
319
            throw new ContainerResolutionException("Targeted [$class] is not instantiable");
320
        }
321
322
        $constructor = $reflector->getConstructor();
323
324
        // If there are no constructors, that means there are no dependencies then
325
        // we can just resolve the instances of the objects right away, without
326
        // resolving any other types or dependencies out of these containers.
327
        if (null === $constructor) {
328
            return $reflector->newInstance();
329
        }
330
331
        // Once we have all the constructor's parameters we can create each of the
332
        // dependency instances and then use the reflection instances to make a
333
        // new instance of this class, injecting the created dependencies in.
334
        // this will be handled in a recursive way...
335
        try {
336
            $instances = $this->autowireArguments($constructor, $args);
337
        } catch (MissingServiceException $e) {
338
            // Resolve default pararamters on class, if paramter was not given and
339
            // default paramter exists, why not let's use it.
340
            foreach ($constructor->getParameters() as $position => $parameter) {
341
                try {
342
                    if (!(isset($args[$position]) || isset($args[$parameter->name]))) {
343
                        $args[$position] = Reflection::getParameterDefaultValue($parameter);
344
                    }
345
                } catch (ReflectionException $e) {
346
                    continue;
347
                }
348
            }
349
350
            return $this->createInstance($class, $args);
351
        }
352
353
        return $reflector->newInstanceArgs($instances);
354
    }
355
356
    /**
357
     * {@inheritdoc}
358
     */
359
    public function runScope(array $bindings, callable $scope)
360
    {
361
        $cleanup = $previous = [];
362
363
        foreach ($bindings as $alias => $resolver) {
364
            if ($this->has($alias)) {
365
                $previous[$alias] = $this->get($alias);
366
367
                continue;
368
            }
369
370
            $cleanup[] = $alias;
371
            $this->addService($alias, $resolver);
372
        }
373
374
        try {
375
            return $scope(...[&$this]);
376
        } finally {
377
            foreach (\array_reverse($previous) as $alias => $resolver) {
378
                $this->instances[$alias] = $resolver;
379
            }
380
381
            foreach ($cleanup as $alias) {
382
                $this->removeService($alias);
383
            }
384
        }
385
    }
386
387
    /**
388
     * {@inheritdoc}
389
     */
390
    public function make(string $alias, ...$parameters)
391
    {
392
        try {
393
            return $this->getService($alias);
394
        } catch (MissingServiceException $e) {
395
            // Allow passing arrays or individual lists of dependencies
396
            if (isset($parameters[0]) && \is_array($parameters[0]) && \count($parameters) === 1) {
397
                $parameters = \array_shift($parameters);
398
            }
399
400
            //Automatically create instance
401
            if (Validators::isType($alias)) {
402
                try {
403
                    $instance = $this->getByType($alias);
404
                } catch (MissingServiceException $e) {
405
                    $instance = $this->createInstance($alias, $parameters);
406
                }
407
408
                $this->callInjects($instance); // Call injectors on the new class instance.
409
410
                return $instance;
411
            }
412
        }
413
414
        throw new NotFoundServiceException(\sprintf('Service [%s] is not found in container', $alias));
415
    }
416
417
    /**
418
     * {@inheritdoc}
419
     */
420
    public function hasMethodBinding($method): bool
421
    {
422
        return isset($this->methods[$method]);
423
    }
424
425
    /**
426
     * Resolve callable methods.
427
     *
428
     * @param string $abstract
429
     * @param string $concrete
430
     * @param string $type
431
     */
432
    private function resolveMethod(string $abstract, string $concrete, string $type): void
433
    {
434
        if (!$this->hasMethodBinding($concrete)) {
435
            $this->types[$abstract] = $type;
436
        }
437
438
        if (($expectedType = $this->getServiceType($abstract)) && !\is_a($type, $expectedType, true)) {
439
            throw new ContainerResolutionException(
440
                "Service '$abstract' must be instance of $expectedType, " .
441
                ($type ? "$type given." : 'add typehint to closure.')
442
            );
443
        }
444
    }
445
446
    /**
447
     * Resolve wiring classes + interfaces.
448
     *
449
     * @param string $name
450
     * @param mixed  $class
451
     */
452
    private function resolveWiring(string $name, $class): void
453
    {
454
        $all = [];
455
456
        foreach (\class_parents($class) + \class_implements($class) + [$class] as $class) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
457
            $all[$class][] = $name;
458
        }
459
460
        foreach ($all as $class => $names) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
461
            $this->wiring[$class] = \array_filter([
462
                \array_diff($names, $this->findByType($class) ?? [], $this->findByTag($class) ?? []),
463
            ]);
464
        }
465
    }
466
467
    /**
468
     * Get the method to be bounded.
469
     *
470
     * @param array|string $method
471
     *
472
     * @return null|ReflectionType
473
     */
474
    private function parseBindMethod($method): ?ReflectionType
475
    {
476
        return Callback::toReflection($method)->getReturnType();
477
    }
478
479
    private function autowireArguments(ReflectionFunctionAbstract $function, array $args = []): array
480
    {
481
        return Resolver::autowireArguments($function, $args, function (string $type, bool $single) {
482
            return $single
483
                ? $this->getByType($type)
484
                : \array_map([$this, 'get'], $this->findAutowired($type));
485
        });
486
    }
487
488
    /**
489
     * Get the Closure or class to be used when building a type.
490
     *
491
     * @param mixed $abstract
492
     *
493
     * @return string
494
     */
495
    private function parseServiceType($abstract): string
496
    {
497
        if ($abstract instanceof Closure) {
498
            /** @var ReflectionFunction $tmp */
499
            if ($tmp = $this->parseBindMethod($abstract)) {
0 ignored issues
show
Bug introduced by
$abstract of type Closure is incompatible with the type array|string expected by parameter $method of Biurad\DependencyInjecti...iner::parseBindMethod(). ( Ignorable by Annotation )

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

499
            if ($tmp = $this->parseBindMethod(/** @scrutinizer ignore-type */ $abstract)) {
Loading history...
500
                return $tmp->getName();
501
            }
502
503
            return '';
504
        }
505
506
        return \get_class($abstract);
507
    }
508
509
    private function getServiceMethods(?array $methods): array
510
    {
511
        return \array_flip(\array_filter(
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_flip(array_...on(...) { /* ... */ })) could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
512
            $methods,
513
            function ($s) {
514
                return \preg_match('#^createService.#', $s);
515
            }
516
        ));
517
    }
518
}
519