PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 All 200 releases
betterdocs / includes / Dependencies / DI / Definition / Source / AnnotationBasedAutowiring.php

AnnotationBasedAutowiring.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.2, at includes/Dependencies/DI/Definition/Source/AnnotationBasedAutowiring.php

294 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:ignoreFile -- Bundled third-party (Mozart) dependency; exempt from plugin coding standards.
3
4 declare(strict_types=1);
5
6 namespace WPDeveloper\BetterDocs\Dependencies\DI\Definition\Source;
7
8 use WPDeveloper\BetterDocs\Dependencies\DI\Annotation\Inject;
9 use WPDeveloper\BetterDocs\Dependencies\DI\Annotation\Injectable;
10 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\Exception\InvalidAnnotation;
11 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\ObjectDefinition;
12 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\ObjectDefinition\MethodInjection;
13 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\ObjectDefinition\PropertyInjection;
14 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\Reference;
15 use Doctrine\Common\Annotations\AnnotationRegistry;
16 use Doctrine\Common\Annotations\Reader;
17 use Doctrine\Common\Annotations\SimpleAnnotationReader;
18 use InvalidArgumentException;
19 use WPDeveloper\BetterDocs\Dependencies\PhpDocReader\PhpDocReader;
20 use ReflectionClass;
21 use ReflectionMethod;
22 use ReflectionParameter;
23 use ReflectionProperty;
24 use UnexpectedValueException;
25
26 /**
27 * Provides WPDeveloper\BetterDocs\Dependencies\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 */
34 class AnnotationBasedAutowiring implements DefinitionSource, Autowiring
35 {
36 /**
37 * @var Reader
38 */
39 private $annotationReader;
40
41 /**
42 * @var WPDeveloper\BetterDocs\Dependencies\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 /** @var Inject $annotation */
125 $annotation = $this->getAnnotationReader()->getPropertyAnnotation($property, 'WPDeveloper\BetterDocs\Dependencies\DI\Annotation\Inject');
126 if ($annotation === null) {
127 return;
128 }
129
130 // @Inject("name") or look for @var content
131 $entryName = $annotation->getName() ?: $this->getPhpDocReader()->getPropertyClass($property);
132
133 if ($entryName === null) {
134 throw new InvalidAnnotation(sprintf(
135 '@Inject found on property %s::%s but unable to guess what to inject, use a @var annotation',
136 $property->getDeclaringClass()->getName(),
137 $property->getName()
138 ));
139 }
140
141 $definition->addPropertyInjection(
142 new PropertyInjection($property->getName(), new Reference($entryName), $classname)
143 );
144 }
145
146 /**
147 * Browse the object's methods looking for annotated methods.
148 */
149 private function readMethods(ReflectionClass $class, ObjectDefinition $objectDefinition)
150 {
151 // This will look in all the methods, including those of the parent classes
152 foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
153 if ($method->isStatic()) {
154 continue;
155 }
156
157 $methodInjection = $this->getMethodInjection($method);
158
159 if (! $methodInjection) {
160 continue;
161 }
162
163 if ($method->isConstructor()) {
164 $objectDefinition->completeConstructorInjection($methodInjection);
165 } else {
166 $objectDefinition->completeFirstMethodInjection($methodInjection);
167 }
168 }
169 }
170
171 /**
172 * @param ReflectionMethod $method
173 *
174 * @return MethodInjection|null
175 */
176 private function getMethodInjection(ReflectionMethod $method)
177 {
178 // Look for @Inject annotation
179 /** @var $annotation Inject|null */
180 try {
181 $annotation = $this->getAnnotationReader()->getMethodAnnotation($method, 'WPDeveloper\BetterDocs\Dependencies\DI\Annotation\Inject');
182 } catch (InvalidAnnotation $e) {
183 throw new InvalidAnnotation(sprintf(
184 '@Inject annotation on %s::%s is malformed. %s',
185 $method->getDeclaringClass()->getName(),
186 $method->getName(),
187 $e->getMessage()
188 ), 0, $e);
189 }
190 $annotationParameters = $annotation ? $annotation->getParameters() : [];
191
192 // @Inject on constructor is implicit
193 if (! ($annotation || $method->isConstructor())) {
194 return null;
195 }
196
197 $parameters = [];
198 foreach ($method->getParameters() as $index => $parameter) {
199 $entryName = $this->getMethodParameter($index, $parameter, $annotationParameters);
200
201 if ($entryName !== null) {
202 $parameters[$index] = new Reference($entryName);
203 }
204 }
205
206 if ($method->isConstructor()) {
207 return MethodInjection::constructor($parameters);
208 }
209
210 return new MethodInjection($method->getName(), $parameters);
211 }
212
213 /**
214 * @param int $parameterIndex
215 * @param ReflectionParameter $parameter
216 * @param array $annotationParameters
217 *
218 * @return string|null Entry name or null if not found.
219 */
220 private function getMethodParameter($parameterIndex, ReflectionParameter $parameter, array $annotationParameters)
221 {
222 // @Inject has definition for this parameter (by index, or by name)
223 if (isset($annotationParameters[$parameterIndex])) {
224 return $annotationParameters[$parameterIndex];
225 }
226 if (isset($annotationParameters[$parameter->getName()])) {
227 return $annotationParameters[$parameter->getName()];
228 }
229
230 // Skip optional parameters if not explicitly defined
231 if ($parameter->isOptional()) {
232 return null;
233 }
234
235 // Try to use the type-hinting
236 $parameterClass = $parameter->getType() && !$parameter->getType()->isBuiltin() ? new ReflectionClass($parameter->getType()->getName()) : null;
237 if ($parameterClass) {
238 return $parameterClass->getName();
239 }
240
241 // Last resort, look for @param tag
242 return $this->getPhpDocReader()->getParameterClass($parameter);
243 }
244
245 /**
246 * @return Reader The annotation reader
247 */
248 public function getAnnotationReader()
249 {
250 if ($this->annotationReader === null) {
251 AnnotationRegistry::registerLoader('class_exists');
252 $this->annotationReader = new SimpleAnnotationReader();
253 $this->annotationReader->addNamespace('WPDeveloper\BetterDocs\Dependencies\DI\Annotation');
254 }
255
256 return $this->annotationReader;
257 }
258
259 /**
260 * @return WPDeveloper\BetterDocs\Dependencies\PhpDocReader
261 */
262 private function getPhpDocReader()
263 {
264 if ($this->phpDocReader === null) {
265 $this->phpDocReader = new PhpDocReader($this->ignorePhpDocErrors);
266 }
267
268 return $this->phpDocReader;
269 }
270
271 private function readInjectableAnnotation(ReflectionClass $class, ObjectDefinition $definition)
272 {
273 try {
274 /** @var Injectable|null $annotation */
275 $annotation = $this->getAnnotationReader()
276 ->getClassAnnotation($class, 'WPDeveloper\BetterDocs\Dependencies\DI\Annotation\Injectable');
277 } catch (UnexpectedValueException $e) {
278 throw new InvalidAnnotation(sprintf(
279 'Error while reading @Injectable on %s: %s',
280 $class->getName(),
281 $e->getMessage()
282 ), 0, $e);
283 }
284
285 if (! $annotation) {
286 return;
287 }
288
289 if ($annotation->isLazy() !== null) {
290 $definition->setLazy($annotation->isLazy());
291 }
292 }
293 }
294