Completed
Push — master ( 3f1d1d...0341ae )
by Matze
05:15
created

ServiceDefinition::processService()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2.003

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 10
cts 11
cp 0.9091
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 10
nc 2
nop 2
crap 2.003
1
<?php
2
3
namespace BrainExe\Core\Annotations\Builder;
4
5
use BrainExe\Core\Annotations\Inject;
6
use BrainExe\Core\Annotations\Service;
7
use BrainExe\Core\Annotations\ServiceIdGenerator;
8
use Doctrine\Common\Annotations\Reader;
9
use InvalidArgumentException;
10
use ReflectionClass;
11
use ReflectionMethod;
12
use Symfony\Component\DependencyInjection\ContainerBuilder;
13
use Symfony\Component\DependencyInjection\ContainerInterface;
14
use Symfony\Component\DependencyInjection\Definition;
15
use Symfony\Component\DependencyInjection\Reference;
16
use Symfony\Component\ExpressionLanguage\Expression;
17
18
/**
19
 * @author Matthias Dötsch <[email protected]>
20
 */
21
class ServiceDefinition
22
{
23
    /**
24
     * @var Reader
25
     */
26
    protected $reader;
27
28
    /**
29
     * @var ServiceIdGenerator
30
     */
31
    private $serviceIdGenerator;
32
33
    /**
34
     * @var ContainerBuilder
35
     */
36
    protected $container;
37
38
    /**
39
     * @param ContainerBuilder $container
40
     * @param Reader $reader
41
     */
42 1
    public function __construct(ContainerBuilder $container, Reader $reader)
43
    {
44 1
        $this->reader = $reader;
45 1
        $this->serviceIdGenerator = new ServiceIdGenerator();
46 1
        $this->container = $container;
47 1
    }
48
49
    /**
50
     * @param ReflectionClass $reflectionClass
51
     * @param Service $annotation
52
     * @param Definition $definition
53
     * @return array
54
     */
55 1
    public function build(ReflectionClass $reflectionClass, Service $annotation, Definition $definition)
56
    {
57 1
        $constructor = $reflectionClass->getConstructor();
58 1
        if (null !== $constructor) {
59 1
            $this->processConstructor($constructor, $definition);
60
        }
61
62 1
        $this->processService($annotation, $definition);
63 1
        $this->processMethods($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC), $definition);
64 1
        $this->processTags($reflectionClass, $annotation, $definition);
65
66 1
        $serviceId = $annotation->value ?: $annotation->name;
67 1
        if (empty($serviceId)) {
68 1
            $serviceId = $this->serviceIdGenerator->generate($reflectionClass->getName());
69
        }
70
71 1
        return [$this->setupDefinition($definition, $serviceId) ?? $serviceId, $definition];
72
    }
73
74
    /**
75
     * @param Definition $definition
76
     * @return string null
77
     */
78 1
    public function setupDefinition(Definition $definition, string $serviceId)
79
    {
80 1
        return null;
81
    }
82
83
    /**
84
     * @param string|string[] $value
85
     * @return Reference[]|Reference|Expression
86
     */
87 1
    private function resolveServices($value)
88
    {
89 1
        if (is_array($value)) {
90
            return array_map([$this, 'resolveServices'], $value);
91 1
        } elseif ('=' === $value[0]) {
92 1
            return new Expression(mb_substr($value, 1));
93 1
        } elseif ('@' === $value[0]) {
94 1
            return $this->getValueReference($value);
95
        }
96
97 1
        return $value;
98
    }
99
100
    /**
101
     * @param ReflectionMethod $constructor
102
     * @param Definition $definition
103
     */
104 1
    protected function processConstructor(ReflectionMethod $constructor, Definition $definition)
105
    {
106 1
        if ($annotation = $this->reader->getMethodAnnotation($constructor, Inject::class)) {
107
            /** @var Inject $annotation */
108 1
            $arguments = $this->extractArguments(
109
                $constructor,
110
                $annotation,
111 1
                $definition->getArguments()
112
            );
113
114 1
            $definition->setArguments($arguments);
115 1
        } elseif ($constructor->getNumberOfParameters() > 0) {
116 1
            $definition->setArguments(
117 1
                $this->resolveArguments($constructor)
118
            );
119
120 1
            $definition->setAutowired(true);
121
        }
122 1
    }
123
124
    /**
125
     * @param ReflectionMethod[] $methods
126
     * @param Definition $definition
127
     */
128 1
    protected function processMethods(array $methods, Definition $definition)
129
    {
130 1
        foreach ($methods as $method) {
131 1
            if ($method->isConstructor()) {
132 1
                continue;
133
            }
134 1
            $this->processMethod($definition, $method);
135
        }
136 1
    }
137
138
    /**
139
     * @param ReflectionMethod $method
140
     * @param Inject $annotation
141
     * @param array $arguments
142
     * @return array
143
     */
144 1
    private function extractArguments(
145
        ReflectionMethod $method,
146
        Inject $annotation,
147
        array $arguments = []
148
    ) : array {
149 1
        $values = [];
150 1
        if (is_string($annotation->value)) {
151 1
            $values = [$annotation->value];
152 1
        } elseif (is_array($annotation->value)) {
153 1
            $values = $annotation->value;
154
        }
155
156 1
        return $this->resolveArguments($method, $values, $arguments);
157
    }
158
159
    /**
160
     * @param ReflectionMethod $method
161
     * @param array $values
162
     * @param array $arguments
163
     * @return array
164
     */
165 1
    private function resolveArguments(
166
        ReflectionMethod $method,
167
        array $values = [],
168
        array $arguments = []
169
    ) : array {
170 1
        foreach ($method->getParameters() as $index => $parameter) {
171 1
            $name = $parameter->getName();
172 1
            if (!empty($values[$name])) {
173 1
                $arguments[$index] = $this->resolveServices($values[$name]);
174 1
            } elseif (isset($values[$index])) {
175 1
                $arguments[$index] = $this->resolveServices($values[$index]);
176 1
            } elseif (!isset($arguments[$index])) {
177 1
                $parameterClass = $parameter->getClass();
178 1
                if ($parameterClass) {
179 1
                    $arguments[$index] = new Reference($parameterClass->getName());
180
                }
181
            }
182
        }
183
184 1
        return $arguments;
185
    }
186
187
    /**
188
     * @param Service $annotation
189
     * @param Definition $definition
190
     */
191 1
    private function processService(Service $annotation, Definition $definition)
192
    {
193 1
        $definition->setAutowired(true);
194 1
        $definition->setPublic($annotation->public);
195 1
        $definition->setLazy($annotation->lazy);
196 1
        $definition->setShared($annotation->shared);
197 1
        $definition->setSynthetic($annotation->synthetic);
198 1
        $definition->setAbstract($annotation->abstract);
199 1
        $this->processConfigurator($annotation, $definition);
200
201 1
        if (isset($annotation->factory)) {
202
            $definition->setFactory($annotation->factory);
203
        }
204 1
    }
205
206
    /**
207
     * @param ReflectionClass $reflectionClass
208
     * @param Service $annotation
209
     * @param Definition $definition
210
     */
211 1
    private function processTags(
212
        ReflectionClass $reflectionClass,
213
        Service $annotation,
214
        Definition $definition
215
    ) {
216 1
        if (empty($annotation->tags)) {
217 1
            return;
218
        }
219
220 1
        foreach ($annotation->tags as $tag) {
221 1
            if (!isset($tag['name'])) {
222
                throw new InvalidArgumentException(
223
                    sprintf(
224
                        'A "tags" entry is missing a "name" key must be an array for class "%s" in %s.',
225
                        $reflectionClass->getName(),
226
                        $reflectionClass->getFileName()
227
                    )
228
                );
229
            }
230 1
            $name = $tag['name'];
231 1
            unset($tag['name']);
232
233 1
            $definition->addTag($name, $tag);
234
        }
235 1
    }
236
237
    /**
238
     * @param string $value
239
     * @return Reference
240
     */
241 1
    private function getValueReference($value) : Reference
242
    {
243 1
        if (0 === strpos($value, '@?')) {
244
            $value           = substr($value, 2);
245
            $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
246
        } else {
247 1
            $value           = substr($value, 1);
248 1
            $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
249
        }
250
251
        // mark reference as strict
252 1
        if ('=' === substr($value, -1)) {
253
            $value  = substr($value, 0, -1);
254
            $strict = false;
255
        } else {
256 1
            $strict = true;
257
        }
258
259 1
        return new Reference($value, $invalidBehavior, $strict);
0 ignored issues
show
Unused Code introduced by
The call to Reference::__construct() has too many arguments starting with $strict.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
260
    }
261
262
    /**
263
     * @param Service $annotation
264
     * @param Definition $definition
265
     */
266 1
    private function processConfigurator(Service $annotation, Definition $definition)
267
    {
268 1
        if (!isset($annotation->configurator)) {
269 1
            return;
270
        }
271
272
        if (is_string($annotation->configurator)) {
273
            $definition->setConfigurator($annotation->configurator);
274
        } else {
275
            $definition->setConfigurator([
276
                $this->resolveServices($annotation->configurator[0]),
277
                $annotation->configurator[1]
278
            ]);
279
        }
280
    }
281
282
    /**
283
     * @param Definition $definition
284
     * @param ReflectionMethod $method
285
     */
286 1
    protected function processMethod(Definition $definition, ReflectionMethod $method)
287
    {
288
        /** @var Inject $annotation */
289 1
        $annotation = $this->reader->getMethodAnnotation($method, Inject::class);
290 1
        if ($annotation) {
291 1
            $arguments = $this->extractArguments(
292
                $method,
293
                $annotation
294
            );
295 1
            $definition->addMethodCall($method->getName(), $arguments);
296
        }
297 1
    }
298
}
299