|
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 |
@@ -192,7 +194,7 @@ public static function parse(string $context, |
192 | 194 | // Only keep lines that don't match the status code pattern in the description |
193 | 195 | $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)))); |
194 | 196 |
|
195 | | - if ($paramTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode && $psalmParamTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) { |
| 197 | + if ($paramTag instanceof ParamTagValueNode && $psalmParamTag instanceof ParamTagValueNode) { |
196 | 198 | try { |
197 | 199 | $type = OpenApiType::resolve( |
198 | 200 | $context . ': @param: ' . $psalmParamTag->parameterName, |
@@ -221,9 +223,9 @@ public static function parse(string $context, |
221 | 223 | ); |
222 | 224 | } |
223 | 225 |
|
224 | | - } elseif ($psalmParamTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) { |
| 226 | + } elseif ($psalmParamTag instanceof ParamTagValueNode) { |
225 | 227 | $type = OpenApiType::resolve($context . ': @param: ' . $methodParameterName, $definitions, $psalmParamTag); |
226 | | - } elseif ($paramTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) { |
| 228 | + } elseif ($paramTag instanceof ParamTagValueNode) { |
227 | 229 | $type = OpenApiType::resolve($context . ': @param: ' . $methodParameterName, $definitions, $paramTag); |
228 | 230 | } elseif ($allowMissingDocs) { |
229 | 231 | $type = OpenApiType::resolve($context . ': $' . $methodParameterName . ': ' . $methodParameterName, $definitions, $methodParameter->type); |
@@ -286,14 +288,14 @@ public static function parse(string $context, |
286 | 288 | Logger::warning($context, 'Summary ends with a punctuation mark'); |
287 | 289 | } |
288 | 290 |
|
289 | | - $headers = []; |
| 291 | + $codeRequestHeaders = []; |
290 | 292 | foreach ($nodeFinder->findInstanceOf($method->getStmts(), MethodCall::class) as $methodCall) { |
291 | 293 | if ($methodCall->var instanceof PropertyFetch && |
292 | 294 | $methodCall->var->var instanceof Variable && |
293 | 295 | $methodCall->var->var->name === 'this' && |
294 | 296 | $methodCall->var->name->name === 'request') { |
295 | 297 | if ($methodCall->name->name === 'getHeader') { |
296 | | - $headers[] = $methodCall->args[0]->value->value; |
| 298 | + $codeRequestHeaders[] = $methodCall->args[0]->value->value; |
297 | 299 | } |
298 | 300 | if ($methodCall->name->name === 'getParam') { |
299 | 301 | $name = $methodCall->args[0]->value->value; |
@@ -326,7 +328,47 @@ public static function parse(string $context, |
326 | 328 | } |
327 | 329 | } |
328 | 330 |
|
329 | | - return new ControllerMethod($parameters, array_unique($headers), $responses, $responseDescriptions, $methodDescription, $methodSummary, $isDeprecated); |
| 331 | + $attributeRequestHeaders = []; |
| 332 | + /** @var AttributeGroup $attrGroup */ |
| 333 | + foreach ($method->attrGroups as $attrGroup) { |
| 334 | + foreach ($attrGroup->attrs as $attr) { |
| 335 | + if ($attr->name->getLast() === 'RequestHeader') { |
| 336 | + $args = []; |
| 337 | + foreach ($attr->args as $key => $arg) { |
| 338 | + $attrName = $arg->name?->name; |
| 339 | + if ($attrName === null) { |
| 340 | + $attrName = match ($key) { |
| 341 | + 0 => 'name', |
| 342 | + 1 => 'description', |
| 343 | + default => throw new RuntimeException('Should not happen.'), |
| 344 | + }; |
| 345 | + } |
| 346 | + $args[$attrName] = $arg->value->value; |
| 347 | + } |
| 348 | + |
| 349 | + if (array_key_exists($args['name'], $attributeRequestHeaders)) { |
| 350 | + Logger::error($context, 'Request header "' . $args['name'] . '" already documented.'); |
| 351 | + } |
| 352 | + |
| 353 | + $attributeRequestHeaders[$args['name']] = $args['description']; |
| 354 | + } |
| 355 | + } |
| 356 | + } |
| 357 | + |
| 358 | + $undocumentedRequestHeaders = array_diff($codeRequestHeaders, array_keys($attributeRequestHeaders)); |
| 359 | + if ($undocumentedRequestHeaders !== []) { |
| 360 | + Logger::warning($context, 'Undocumented request headers (use the RequestHeader attribute): ' . implode(', ', $undocumentedRequestHeaders)); |
| 361 | + foreach ($undocumentedRequestHeaders as $header) { |
| 362 | + $attributeRequestHeaders[$header] = null; |
| 363 | + } |
| 364 | + } |
| 365 | + |
| 366 | + $unusedRequestHeaders = array_diff(array_keys($attributeRequestHeaders), $codeRequestHeaders); |
| 367 | + if ($unusedRequestHeaders !== []) { |
| 368 | + Logger::error($context, 'Unused request header descriptions: ' . implode(', ', $unusedRequestHeaders)); |
| 369 | + } |
| 370 | + |
| 371 | + return new ControllerMethod($parameters, $attributeRequestHeaders, $responses, $responseDescriptions, $methodDescription, $methodSummary, $isDeprecated); |
330 | 372 | } |
331 | 373 |
|
332 | 374 | } |
0 commit comments