1<?php
2
3declare(strict_types=1);
4
5namespace DI\Definition\Source;
6
7use DI\Annotation\Inject;
8use DI\Annotation\Injectable;
9use DI\Definition\Exception\InvalidAnnotation;
10use DI\Definition\ObjectDefinition;
11use DI\Definition\ObjectDefinition\MethodInjection;
12use DI\Definition\ObjectDefinition\PropertyInjection;
13use DI\Definition\Reference;
14use Doctrine\Common\Annotations\AnnotationRegistry;
15use Doctrine\Common\Annotations\Reader;
16use Doctrine\Common\Annotations\SimpleAnnotationReader;
17use InvalidArgumentException;
18use PhpDocReader\PhpDocReader;
19use ReflectionClass;
20use ReflectionMethod;
21use ReflectionNamedType;
22use ReflectionParameter;
23use ReflectionProperty;
24use UnexpectedValueException;
25
26/**
27 * Provides DI definitions by reading annotations such as @ Inject and @ var annotations.
28 *
29 * Uses Autowiring, Doctrine's Annotations and regex docblock parsing.
30 * This source automatically includes the reflection source.
31 *
32 * @author Matthieu Napoli <matthieu@mnapoli.fr>
33 */
34class AnnotationBasedAutowiring implements DefinitionSource, Autowiring
35{
36    /**
37     * @var Reader
38     */
39    private $annotationReader;
40
41    /**
42     * @var PhpDocReader
43     */
44    private $phpDocReader;
45
46    /**
47     * @var bool
48     */
49    private $ignorePhpDocErrors;
50
51    public function __construct($ignorePhpDocErrors = false)
52    {
53        $this->ignorePhpDocErrors = (bool) $ignorePhpDocErrors;
54    }
55
56    public function autowire(string $name, ObjectDefinition $definition = null)
57    {
58        $className = $definition ? $definition->getClassName() : $name;
59
60        if (!class_exists($className) && !interface_exists($className)) {
61            return $definition;
62        }
63
64        $definition = $definition ?: new ObjectDefinition($name);
65
66        $class = new ReflectionClass($className);
67
68        $this->readInjectableAnnotation($class, $definition);
69
70        // Browse the class properties looking for annotated properties
71        $this->readProperties($class, $definition);
72
73        // Browse the object's methods looking for annotated methods
74        $this->readMethods($class, $definition);
75
76        return $definition;
77    }
78
79    /**
80     * {@inheritdoc}
81     * @throws InvalidAnnotation
82     * @throws InvalidArgumentException The class doesn't exist
83     */
84    public function getDefinition(string $name)
85    {
86        return $this->autowire($name);
87    }
88
89    /**
90     * Autowiring cannot guess all existing definitions.
91     */
92    public function getDefinitions() : array
93    {
94        return [];
95    }
96
97    /**
98     * Browse the class properties looking for annotated properties.
99     */
100    private function readProperties(ReflectionClass $class, ObjectDefinition $definition)
101    {
102        foreach ($class->getProperties() as $property) {
103            if ($property->isStatic()) {
104                continue;
105            }
106            $this->readProperty($property, $definition);
107        }
108
109        // Read also the *private* properties of the parent classes
110        /** @noinspection PhpAssignmentInConditionInspection */
111        while ($class = $class->getParentClass()) {
112            foreach ($class->getProperties(ReflectionProperty::IS_PRIVATE) as $property) {
113                if ($property->isStatic()) {
114                    continue;
115                }
116                $this->readProperty($property, $definition, $class->getName());
117            }
118        }
119    }
120
121    private function readProperty(ReflectionProperty $property, ObjectDefinition $definition, $classname = null)
122    {
123        // Look for @Inject annotation
124        $annotation = $this->getAnnotationReader()->getPropertyAnnotation($property, 'DI\Annotation\Inject');
125        if (!$annotation instanceof Inject) {
126            return;
127        }
128
129        // Try to @Inject("name") or look for @var content
130        $entryName = $annotation->getName() ?: $this->getPhpDocReader()->getPropertyClass($property);
131
132        // Try using PHP7.4 typed properties
133        if (\PHP_VERSION_ID > 70400
134            && $entryName === null
135            && $property->getType() instanceof ReflectionNamedType
136            && (class_exists($property->getType()->getName()) || interface_exists($property->getType()->getName()))
137        ) {
138            $entryName = $property->getType()->getName();
139        }
140
141        if ($entryName === null) {
142            throw new InvalidAnnotation(sprintf(
143                '@Inject found on property %s::%s but unable to guess what to inject, use a @var annotation',
144                $property->getDeclaringClass()->getName(),
145                $property->getName()
146            ));
147        }
148
149        $definition->addPropertyInjection(
150            new PropertyInjection($property->getName(), new Reference($entryName), $classname)
151        );
152    }
153
154    /**
155     * Browse the object's methods looking for annotated methods.
156     */
157    private function readMethods(ReflectionClass $class, ObjectDefinition $objectDefinition)
158    {
159        // This will look in all the methods, including those of the parent classes
160        foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
161            if ($method->isStatic()) {
162                continue;
163            }
164
165            $methodInjection = $this->getMethodInjection($method);
166
167            if (! $methodInjection) {
168                continue;
169            }
170
171            if ($method->isConstructor()) {
172                $objectDefinition->completeConstructorInjection($methodInjection);
173            } else {
174                $objectDefinition->completeFirstMethodInjection($methodInjection);
175            }
176        }
177    }
178
179    /**
180     * @return MethodInjection|null
181     */
182    private function getMethodInjection(ReflectionMethod $method)
183    {
184        // Look for @Inject annotation
185        try {
186            $annotation = $this->getAnnotationReader()->getMethodAnnotation($method, 'DI\Annotation\Inject');
187        } catch (InvalidAnnotation $e) {
188            throw new InvalidAnnotation(sprintf(
189                '@Inject annotation on %s::%s is malformed. %s',
190                $method->getDeclaringClass()->getName(),
191                $method->getName(),
192                $e->getMessage()
193            ), 0, $e);
194        }
195
196        // @Inject on constructor is implicit
197        if (! ($annotation || $method->isConstructor())) {
198            return null;
199        }
200
201        $annotationParameters = $annotation instanceof Inject ? $annotation->getParameters() : [];
202
203        $parameters = [];
204        foreach ($method->getParameters() as $index => $parameter) {
205            $entryName = $this->getMethodParameter($index, $parameter, $annotationParameters);
206
207            if ($entryName !== null) {
208                $parameters[$index] = new Reference($entryName);
209            }
210        }
211
212        if ($method->isConstructor()) {
213            return MethodInjection::constructor($parameters);
214        }
215
216        return new MethodInjection($method->getName(), $parameters);
217    }
218
219    /**
220     * @param int                 $parameterIndex
221     *
222     * @return string|null Entry name or null if not found.
223     */
224    private function getMethodParameter($parameterIndex, ReflectionParameter $parameter, array $annotationParameters)
225    {
226        // @Inject has definition for this parameter (by index, or by name)
227        if (isset($annotationParameters[$parameterIndex])) {
228            return $annotationParameters[$parameterIndex];
229        }
230        if (isset($annotationParameters[$parameter->getName()])) {
231            return $annotationParameters[$parameter->getName()];
232        }
233
234        // Skip optional parameters if not explicitly defined
235        if ($parameter->isOptional()) {
236            return null;
237        }
238
239        // Try to use the type-hinting
240        $parameterType = $parameter->getType();
241        if ($parameterType && $parameterType instanceof ReflectionNamedType && !$parameterType->isBuiltin()) {
242            return $parameterType->getName();
243        }
244
245        // Last resort, look for @param tag
246        return $this->getPhpDocReader()->getParameterClass($parameter);
247    }
248
249    /**
250     * @return Reader The annotation reader
251     */
252    public function getAnnotationReader()
253    {
254        if ($this->annotationReader === null) {
255            AnnotationRegistry::registerLoader('class_exists');
256            $this->annotationReader = new SimpleAnnotationReader();
257            $this->annotationReader->addNamespace('DI\Annotation');
258        }
259
260        return $this->annotationReader;
261    }
262
263    /**
264     * @return PhpDocReader
265     */
266    private function getPhpDocReader()
267    {
268        if ($this->phpDocReader === null) {
269            $this->phpDocReader = new PhpDocReader($this->ignorePhpDocErrors);
270        }
271
272        return $this->phpDocReader;
273    }
274
275    private function readInjectableAnnotation(ReflectionClass $class, ObjectDefinition $definition)
276    {
277        try {
278            /** @var Injectable|null $annotation */
279            $annotation = $this->getAnnotationReader()
280                ->getClassAnnotation($class, 'DI\Annotation\Injectable');
281        } catch (UnexpectedValueException $e) {
282            throw new InvalidAnnotation(sprintf(
283                'Error while reading @Injectable on %s: %s',
284                $class->getName(),
285                $e->getMessage()
286            ), 0, $e);
287        }
288
289        if (! $annotation) {
290            return;
291        }
292
293        if ($annotation->isLazy() !== null) {
294            $definition->setLazy($annotation->isLazy());
295        }
296    }
297}
298