|
7 | 7 |
|
8 | 8 | namespace OpenAPIExtractor; |
9 | 9 |
|
| 10 | +use PhpParser\Node\AttributeGroup; |
10 | 11 | use PhpParser\Node\Expr\MethodCall; |
11 | 12 | use PhpParser\Node\Expr\PropertyFetch; |
12 | 13 | use PhpParser\Node\Expr\Variable; |
|
20 | 21 | use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; |
21 | 22 | use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode; |
22 | 23 | use PHPStan\PhpDocParser\Parser\TokenIterator; |
| 24 | +use RuntimeException; |
23 | 25 |
|
24 | 26 | class ControllerMethod { |
25 | 27 | private const STATUS_CODE_DESCRIPTION_PATTERN = '/^(\d{3}): (.+)$/'; |
26 | 28 |
|
27 | 29 | /** |
28 | 30 | * @param ControllerMethodParameter[] $parameters |
29 | | - * @param list<string> $requestHeaders |
| 31 | + * @param array<string, string> $requestHeaders |
30 | 32 | * @param list<ControllerMethodResponse|null> $responses |
31 | 33 | * @param OpenApiType[] $returns |
32 | 34 | * @param array<int, string> $responseDescription |
@@ -198,7 +200,7 @@ public static function parse(string $context, |
198 | 200 | // Only keep lines that don't match the status code pattern in the description |
199 | 201 | $description = Helpers::cleanDocComment(implode("\n", array_filter(array_filter(explode("\n", $description), static fn (string $line): bool => trim($line) !== ''), static fn (string $line): bool => in_array(preg_match(self::STATUS_CODE_DESCRIPTION_PATTERN, $line), [0, false], true)))); |
200 | 202 |
|
201 | | - if ($paramTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode && $psalmParamTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) { |
| 203 | + if ($paramTag instanceof ParamTagValueNode && $psalmParamTag instanceof ParamTagValueNode) { |
202 | 204 | try { |
203 | 205 | $type = OpenApiType::resolve( |
204 | 206 | $context . ': @param: ' . $psalmParamTag->parameterName, |
@@ -227,9 +229,9 @@ public static function parse(string $context, |
227 | 229 | ); |
228 | 230 | } |
229 | 231 |
|
230 | | - } elseif ($psalmParamTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) { |
| 232 | + } elseif ($psalmParamTag instanceof ParamTagValueNode) { |
231 | 233 | $type = OpenApiType::resolve($context . ': @param: ' . $methodParameterName, $definitions, $psalmParamTag); |
232 | | - } elseif ($paramTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) { |
| 234 | + } elseif ($paramTag instanceof ParamTagValueNode) { |
233 | 235 | $type = OpenApiType::resolve($context . ': @param: ' . $methodParameterName, $definitions, $paramTag); |
234 | 236 | } elseif ($allowMissingDocs) { |
235 | 237 | $type = OpenApiType::resolve($context . ': $' . $methodParameterName . ': ' . $methodParameterName, $definitions, $methodParameter->type); |
@@ -292,14 +294,14 @@ public static function parse(string $context, |
292 | 294 | Logger::warning($context, 'Summary ends with a punctuation mark'); |
293 | 295 | } |
294 | 296 |
|
295 | | - $headers = []; |
| 297 | + $codeRequestHeaders = []; |
296 | 298 | foreach ($nodeFinder->findInstanceOf($method->getStmts(), MethodCall::class) as $methodCall) { |
297 | 299 | if ($methodCall->var instanceof PropertyFetch && |
298 | 300 | $methodCall->var->var instanceof Variable && |
299 | 301 | $methodCall->var->var->name === 'this' && |
300 | 302 | $methodCall->var->name->name === 'request') { |
301 | 303 | if ($methodCall->name->name === 'getHeader') { |
302 | | - $headers[] = $methodCall->args[0]->value->value; |
| 304 | + $codeRequestHeaders[] = $methodCall->args[0]->value->value; |
303 | 305 | } |
304 | 306 | if ($methodCall->name->name === 'getParam') { |
305 | 307 | $name = $methodCall->args[0]->value->value; |
@@ -332,7 +334,47 @@ public static function parse(string $context, |
332 | 334 | } |
333 | 335 | } |
334 | 336 |
|
335 | | - return new ControllerMethod($parameters, array_unique($headers), $responses, $responseDescriptions, $methodDescription, $methodSummary, $isDeprecated); |
| 337 | + $attributeRequestHeaders = []; |
| 338 | + /** @var AttributeGroup $attrGroup */ |
| 339 | + foreach ($method->attrGroups as $attrGroup) { |
| 340 | + foreach ($attrGroup->attrs as $attr) { |
| 341 | + if ($attr->name->getLast() === 'RequestHeader') { |
| 342 | + $args = []; |
| 343 | + foreach ($attr->args as $key => $arg) { |
| 344 | + $attrName = $arg->name?->name; |
| 345 | + if ($attrName === null) { |
| 346 | + $attrName = match ($key) { |
| 347 | + 0 => 'name', |
| 348 | + 1 => 'description', |
| 349 | + default => throw new RuntimeException('Should not happen.'), |
| 350 | + }; |
| 351 | + } |
| 352 | + $args[$attrName] = $arg->value->value; |
| 353 | + } |
| 354 | + |
| 355 | + if (array_key_exists($args['name'], $attributeRequestHeaders)) { |
| 356 | + Logger::error($context, 'Request header "' . $args['name'] . '" already documented.'); |
| 357 | + } |
| 358 | + |
| 359 | + $attributeRequestHeaders[$args['name']] = $args['description']; |
| 360 | + } |
| 361 | + } |
| 362 | + } |
| 363 | + |
| 364 | + $undocumentedRequestHeaders = array_diff($codeRequestHeaders, array_keys($attributeRequestHeaders)); |
| 365 | + if ($undocumentedRequestHeaders !== []) { |
| 366 | + Logger::warning($context, 'Undocumented request headers (use the RequestHeader attribute): ' . implode(', ', $undocumentedRequestHeaders)); |
| 367 | + foreach ($undocumentedRequestHeaders as $header) { |
| 368 | + $attributeRequestHeaders[$header] = null; |
| 369 | + } |
| 370 | + } |
| 371 | + |
| 372 | + $unusedRequestHeaders = array_diff(array_keys($attributeRequestHeaders), $codeRequestHeaders); |
| 373 | + if ($unusedRequestHeaders !== []) { |
| 374 | + Logger::error($context, 'Unused request header descriptions: ' . implode(', ', $unusedRequestHeaders)); |
| 375 | + } |
| 376 | + |
| 377 | + return new ControllerMethod($parameters, $attributeRequestHeaders, $responses, $responseDescriptions, $methodDescription, $methodSummary, $isDeprecated); |
336 | 378 | } |
337 | 379 |
|
338 | 380 | } |
0 commit comments