PluginProbe ʕ •ᴥ•ʔ
Cookiebot by Usercentrics – Automatic Cookie Banner for GDPR/CCPA & Google Consent Mode / 3.10.1
Cookiebot by Usercentrics – Automatic Cookie Banner for GDPR/CCPA & Google Consent Mode v3.10.1
4.7.2 4.7.1 trunk 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 3.0.0 3.0.1 3.1.0 3.10.0 3.10.1 3.11.1 3.11.2 3.11.3 3.2.0 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.5 3.6.6 3.7.0 3.7.1 3.8.0 3.9.0 4.0.0 4.0.1 4.0.2 4.0.3 4.1.0 4.1.1 4.2.0 4.2.1 4.2.10 4.2.11 4.2.12 4.2.13 4.2.14 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.3.10 4.3.11 4.3.12 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.3.7 4.3.7.1 4.3.8 4.3.9 4.3.9.1 4.4.0 4.4.1 4.4.2 4.5.0 4.5.1 4.5.10 4.5.11 4.5.2 4.5.3 4.5.4 4.5.5 4.5.6 4.5.7 4.5.8 4.5.9 4.6.0 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.6.6 4.6.7 4.7.0
cookiebot / addons / inc / Dependencies / PhpDocReader / PhpDocReader.php
cookiebot / addons / inc / Dependencies / PhpDocReader Last commit date
PhpParser 5 years ago AnnotationException.php 5 years ago PhpDocReader.php 5 years ago
PhpDocReader.php
308 lines
1 <?php declare(strict_types=1);
2
3 namespace Cybot\Dependencies\PhpDocReader;
4
5 use Cybot\Dependencies\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