PluginProbe
The Innovative Form Builder – IvyForms / 0.7
The Innovative Form Builder – IvyForms v0.7
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / scope-vendor / php-di / phpdoc-reader / src / PhpDocReader / PhpDocReader.php

PhpDocReader.php in The Innovative Form Builder – IvyForms 0.7, at backend/scope-vendor/php-di/phpdoc-reader/src/PhpDocReader/PhpDocReader.php

308 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php declare(strict_types=1);
2
3 namespace IvyForms\Vendor\PhpDocReader;
4
5 use IvyForms\Vendor\PhpDocReader\PhpParser\UseStatementParser;
6 use ReflectionClass;
7 use ReflectionMethod;
8 use ReflectionParameter;
9 use ReflectionProperty;
10 use Reflector;
11
12 /**
13 * PhpDoc reader
14 */
15 class PhpDocReader
16 {
17 /** @var UseStatementParser */
18 private $parser;
19
20 private const PRIMITIVE_TYPES = [
21 'bool' => 'bool',
22 'boolean' => 'bool',
23 'string' => 'string',
24 'int' => 'int',
25 'integer' => 'int',
26 'float' => 'float',
27 'double' => 'float',
28 'array' => 'array',
29 'object' => 'object',
30 'callable' => 'callable',
31 'resource' => 'resource',
32 'mixed' => 'mixed',
33 'iterable' => 'iterable',
34 ];
35
36 /** @var bool */
37 private $ignorePhpDocErrors;
38
39 /**
40 * @param bool $ignorePhpDocErrors Enable or disable throwing errors when PhpDoc errors occur (when parsing annotations).
41 */
42 public function __construct(bool $ignorePhpDocErrors = false)
43 {
44 $this->parser = new UseStatementParser;
45 $this->ignorePhpDocErrors = $ignorePhpDocErrors;
46 }
47
48 /**
49 * Parse the docblock of the property to get the type (class or primitive type) of the var annotation.
50 *
51 * @return string|null Type of the property (content of var annotation)
52 * @throws AnnotationException
53 */
54 public function getPropertyType(ReflectionProperty $property): ?string
55 {
56 return $this->readPropertyType($property, true);
57 }
58
59 /**
60 * Parse the docblock of the property to get the class of the var annotation.
61 *
62 * @return string|null Type of the property (content of var annotation)
63 * @throws AnnotationException
64 */
65 public function getPropertyClass(ReflectionProperty $property): ?string
66 {
67 return $this->readPropertyType($property, false);
68 }
69
70 private function readPropertyType(ReflectionProperty $property, bool $allowPrimitiveTypes): ?string
71 {
72 // Get the content of the @var annotation
73 $docComment = $property->getDocComment();
74 if (! $docComment) {
75 return null;
76 }
77 if (preg_match('/@var\s+([^\s]+)/', $docComment, $matches)) {
78 [, $type] = $matches;
79 } else {
80 return null;
81 }
82
83 // Ignore primitive types
84 if (isset(self::PRIMITIVE_TYPES[$type])) {
85 if ($allowPrimitiveTypes) {
86 return self::PRIMITIVE_TYPES[$type];
87 }
88 return null;
89 }
90
91 // Ignore types containing special characters ([], <> ...)
92 if (! preg_match('/^[a-zA-Z0-9\\\\_]+$/', $type)) {
93 return null;
94 }
95
96 $class = $property->getDeclaringClass();
97
98 // If the class name is not fully qualified (i.e. doesn't start with a \)
99 if ($type[0] !== '\\') {
100 // Try to resolve the FQN using the class context
101 $resolvedType = $this->tryResolveFqn($type, $class, $property);
102
103 if (! $resolvedType && ! $this->ignorePhpDocErrors) {
104 throw new AnnotationException(sprintf(
105 'The @var annotation on %s::%s contains a non existent class "%s". '
106 . 'Did you maybe forget to add a "use" statement for this annotation?',
107 $class->name,
108 $property->getName(),
109 $type
110 ));
111 }
112
113 $type = $resolvedType;
114 }
115
116 if (! $this->ignorePhpDocErrors && ! $this->classExists($type)) {
117 throw new AnnotationException(sprintf(
118 'The @var annotation on %s::%s contains a non existent class "%s"',
119 $class->name,
120 $property->getName(),
121 $type
122 ));
123 }
124
125 // Remove the leading \ (FQN shouldn't contain it)
126 $type = is_string($type) ? ltrim($type, '\\') : null;
127
128 return $type;
129 }
130
131 /**
132 * Parse the docblock of the property to get the type (class or primitive type) of the param annotation.
133 *
134 * @return string|null Type of the property (content of var annotation)
135 * @throws AnnotationException
136 */
137 public function getParameterType(ReflectionParameter $parameter): ?string
138 {
139 return $this->readParameterClass($parameter, true);
140 }
141
142 /**
143 * Parse the docblock of the property to get the class of the param annotation.
144 *
145 * @return string|null Type of the property (content of var annotation)
146 * @throws AnnotationException
147 */
148 public function getParameterClass(ReflectionParameter $parameter): ?string
149 {
150 return $this->readParameterClass($parameter, false);
151 }
152
153 private function readParameterClass(ReflectionParameter $parameter, bool $allowPrimitiveTypes): ?string
154 {
155 // Use reflection
156 $parameterType = $parameter->getType();
157 if ($parameterType && $parameterType instanceof \ReflectionNamedType && ! $parameterType->isBuiltin()) {
158 return $parameterType->getName();
159 }
160
161 $parameterName = $parameter->name;
162 // Get the content of the @param annotation
163 $method = $parameter->getDeclaringFunction();
164 $docComment = $method->getDocComment();
165 if (! $docComment) {
166 return null;
167 }
168 if (preg_match('/@param\s+([^\s]+)\s+\$' . $parameterName . '/', $docComment, $matches)) {
169 [, $type] = $matches;
170 } else {
171 return null;
172 }
173
174 // Ignore primitive types
175 if (isset(self::PRIMITIVE_TYPES[$type])) {
176 if ($allowPrimitiveTypes) {
177 return self::PRIMITIVE_TYPES[$type];
178 }
179 return null;
180 }
181
182 // Ignore types containing special characters ([], <> ...)
183 if (! preg_match('/^[a-zA-Z0-9\\\\_]+$/', $type)) {
184 return null;
185 }
186
187 $class = $parameter->getDeclaringClass();
188
189 // If the class name is not fully qualified (i.e. doesn't start with a \)
190 if ($type[0] !== '\\') {
191 // Try to resolve the FQN using the class context
192 $resolvedType = $this->tryResolveFqn($type, $class, $parameter);
193
194 if (! $resolvedType && ! $this->ignorePhpDocErrors) {
195 throw new AnnotationException(sprintf(
196 'The @param annotation for parameter "%s" of %s::%s contains a non existent class "%s". '
197 . 'Did you maybe forget to add a "use" statement for this annotation?',
198 $parameterName,
199 $class->name,
200 $method->name,
201 $type
202 ));
203 }
204
205 $type = $resolvedType;
206 }
207
208 if (! $this->ignorePhpDocErrors && ! $this->classExists($type)) {
209 throw new AnnotationException(sprintf(
210 'The @param annotation for parameter "%s" of %s::%s contains a non existent class "%s"',
211 $parameterName,
212 $class->name,
213 $method->name,
214 $type
215 ));
216 }
217
218 // Remove the leading \ (FQN shouldn't contain it)
219 $type = is_string($type) ? ltrim($type, '\\') : null;
220
221 return $type;
222 }
223
224 /**
225 * Attempts to resolve the FQN of the provided $type based on the $class and $member context.
226 *
227 * @return string|null Fully qualified name of the type, or null if it could not be resolved
228 */
229 private function tryResolveFqn(string $type, ReflectionClass $class, Reflector $member): ?string
230 {
231 $alias = ($pos = strpos($type, '\\')) === false ? $type : substr($type, 0, $pos);
232 $loweredAlias = strtolower($alias);
233
234 // Retrieve "use" statements
235 $uses = $this->parser->parseUseStatements($class);
236
237 if (isset($uses[$loweredAlias])) {
238 // Imported classes
239 if ($pos !== false) {
240 return $uses[$loweredAlias] . substr($type, $pos);
241 }
242 return $uses[$loweredAlias];
243 }
244
245 if ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
246 return $class->getNamespaceName() . '\\' . $type;
247 }
248
249 if (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
250 // Class namespace
251 return $uses['__NAMESPACE__'] . '\\' . $type;
252 }
253
254 if ($this->classExists($type)) {
255 // No namespace
256 return $type;
257 }
258
259 // If all fail, try resolving through related traits
260 return $this->tryResolveFqnInTraits($type, $class, $member);
261 }
262
263 /**
264 * Attempts to resolve the FQN of the provided $type based on the $class and $member context, specifically searching
265 * through the traits that are used by the provided $class.
266 *
267 * @return string|null Fully qualified name of the type, or null if it could not be resolved
268 */
269 private function tryResolveFqnInTraits(string $type, ReflectionClass $class, Reflector $member): ?string
270 {
271 /** @var ReflectionClass[] $traits */
272 $traits = [];
273
274 // Get traits for the class and its parents
275 while ($class) {
276 $traits = array_merge($traits, $class->getTraits());
277 $class = $class->getParentClass();
278 }
279
280 foreach ($traits as $trait) {
281 // Eliminate traits that don't have the property/method/parameter
282 if ($member instanceof ReflectionProperty && ! $trait->hasProperty($member->name)) {
283 continue;
284 }
285 if ($member instanceof ReflectionMethod && ! $trait->hasMethod($member->name)) {
286 continue;
287 }
288 if ($member instanceof ReflectionParameter && ! $trait->hasMethod($member->getDeclaringFunction()->name)) {
289 continue;
290 }
291
292 // Run the resolver again with the ReflectionClass instance for the trait
293 $resolvedType = $this->tryResolveFqn($type, $trait, $member);
294
295 if ($resolvedType) {
296 return $resolvedType;
297 }
298 }
299
300 return null;
301 }
302
303 private function classExists(string $class): bool
304 {
305 return class_exists($class) || interface_exists($class);
306 }
307 }
308