ReflectionAutowire   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 160
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 48
c 1
b 0
f 0
dl 0
loc 160
ccs 52
cts 52
cp 1
rs 10
wmc 22

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getDependencies() 0 11 4
A getParamType() 0 16 4
A determineDependencies() 0 23 5
A __construct() 0 4 1
A __invoke() 0 3 1
A instantiate() 0 12 2
A extractParamAnnotations() 0 15 5
1
<?php declare(strict_types=1);
2
3
namespace Jasny\Container\Autowire;
4
5
use Jasny\Container\Exception\AutowireException;
6
use Jasny\ReflectionFactory\ReflectionFactory;
7
use Jasny\ReflectionFactory\ReflectionFactoryInterface;
8
use Psr\Container\ContainerInterface as Psr11Container;
9
10
/**
11
 * Autowiring using reflection and annotations.
12
 */
13
class ReflectionAutowire implements AutowireInterface
14
{
15
    /**
16
     * @var Psr11Container
17
     */
18
    protected $container;
19
20
    /**
21
     * @var ReflectionFactoryInterface
22
     */
23
    protected $reflection;
24
25
    /**
26
     * ReflectionAutowire constructor.
27
     *
28
     * @param Psr11Container              $container
29
     * @param ReflectionFactoryInterface  $reflection
30
     */
31 10
    public function __construct(Psr11Container $container, ReflectionFactoryInterface $reflection = null)
32
    {
33 10
        $this->container = $container;
34 10
        $this->reflection = $reflection ?? new ReflectionFactory();
35 10
    }
36
37
38
    /**
39
     * Get annotations for the constructor parameters.
40
     * Annotated parameter types are not considered. Turning the class to a FQCN is more work than it's worth.
41
     *
42
     * @param string $docComment
43
     * @return array
44
     */
45 5
    protected function extractParamAnnotations(string $docComment): array
46
    {
47 5
        $pattern = '/@param(?:\s+([^$"]\S+))?(?:\s+\$(\w+))?(?:\s+"([^"]++)")?/';
48
49 5
        if (!(bool)preg_match_all($pattern, $docComment, $matches, PREG_SET_ORDER)) {
50 3
            return [];
51
        }
52
53 2
        $annotations = [];
54
55 2
        foreach ($matches as $index => $match) {
56 2
            $annotations[$index] = isset($match[3]) && $match[3] !== '' ? $match[3] : null;
57
        }
58
59 2
        return $annotations;
60
    }
61
62
    /**
63
     * Get the declared type of a parameter.
64
     *
65
     * @param \ReflectionClass     $class
66
     * @param \ReflectionParameter $param
67
     * @return string
68
     */
69 7
    protected function getParamType(\ReflectionClass $class, \ReflectionParameter $param): string
70
    {
71 7
        $reflType = $param->getType();
72
73 7
        if ($reflType === null || !$reflType instanceof \ReflectionNamedType) {
74 1
            throw new AutowireException("Unable to autowire " . $class->getName() . ": "
75 1
                . "Unknown type for parameter '" . $param->getName() . "'.");
76
        }
77
78 7
        if ($reflType->isBuiltin()) {
79 1
            throw new AutowireException("Unable to autowire " . $class->getName() . ": "
80 1
                . "Build-in type '" . $reflType->getName() . "' for parameter '" . $param->getName() . "' can't be "
81 1
                . "used as container id. Please specify via @param.");
82
        }
83
84 7
        return $reflType->getName();
85
    }
86
87
    /**
88
     * Get all dependencies for a class constructor.
89
     *
90
     * @param \ReflectionClass $class
91
     * @param int $skip Number of parameters to skip
92
     * @return array[]
93
     * @throws \ReflectionException
94
     */
95 9
    protected function determineDependencies(\ReflectionClass $class, int $skip): array
96
    {
97 9
        if (!$class->hasMethod('__construct')) {
98 1
            return [];
99
        }
100
101 8
        $constructor = $class->getMethod('__construct');
102 8
        $docComment = $constructor->getDocComment();
103 8
        $annotations = is_string($docComment) ? $this->extractParamAnnotations($docComment) : [];
104
105 8
        $identifiers = [];
106
107 8
        $params = $constructor->getParameters();
108 8
        $consideredParams = $skip === 0 ? $params : array_slice($params, $skip, null, true);
109
110 8
        foreach ($consideredParams as $index => $param) {
111 8
            $identifiers[$index] = [
112 8
                'key' => $annotations[$index] ?? $this->getParamType($class, $param),
113 8
                'optional' => $param->allowsNull(),
114
            ];
115
        }
116
117 6
        return $identifiers;
118
    }
119
120
    /**
121
     * Get dependencies from the container
122
     *
123
     * @param array[] $identifiers
124
     * @return array
125
     */
126 7
    protected function getDependencies(array $identifiers): array
127
    {
128 7
        $dependencies = [];
129
130 7
        foreach ($identifiers as $index => ['key' => $key, 'optional' => $optional]) {
131 6
            $dependencies[$index] = !(bool)$optional || $this->container->has($key)
132 6
                ? $this->container->get($key)
133 1
                : null;
134
        };
135
136 7
        return $dependencies;
137
    }
138
139
    /**
140
     * Instantiate a new object, automatically injecting dependencies
141
     *
142
     * @param string $class
143
     * @param mixed  ...$args  Additional arguments are passed to the constructor directly.
144
     * @return object
145
     * @throws AutowireException
146
     */
147 10
    public function instantiate(string $class, ...$args)
148
    {
149
        try {
150 10
            $refl = $this->reflection->reflectClass($class);
151
152 9
            $dependencyIds = $this->determineDependencies($refl, count($args));
153 7
            $dependencies = $args + $this->getDependencies($dependencyIds);
154 3
        } catch (\ReflectionException $exception) {
155 1
            throw new AutowireException("Unable to autowire {$class}: " . $exception->getMessage(), 0, $exception);
156
        }
157
158 7
        return $refl->newInstanceArgs($dependencies);
159
    }
160
161
    /**
162
     * Alias of `instantiate` method
163
     *
164
     * @param string $class
165
     * @param mixed  ...$args  Additional arguments are passed to the constructor directly.
166
     * @return object
167
     * @throws AutowireException
168
     * @throws \ReflectionException
169
     */
170 1
    final public function __invoke($class, ...$args)
171
    {
172 1
        return $this->instantiate($class, ...$args);
173
    }
174
}
175