Skip to content

Commit 0d32b22

Browse files
author
jakob.bennemann
committed
fix: responses for HTTP 204
1 parent 11b56b4 commit 0d32b22

File tree

3 files changed

+165
-43
lines changed

3 files changed

+165
-43
lines changed

src/Services/EnhancedResponseAnalyzer.php

Lines changed: 102 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public function analyzeControllerMethodResponses(string $controller, string $met
8080
if ($filename && file_exists($filename)) {
8181
$fileContent = file_get_contents($filename);
8282
$ast = $this->parser->parse($fileContent);
83-
83+
8484
if (! $ast) {
8585
return $this->getDefaultResponses($controller, $method);
8686
}
@@ -126,22 +126,23 @@ public function __construct(
126126
) {}
127127

128128
private bool $insideTargetMethod = false;
129-
129+
130130
public function enterNode(Node $node)
131131
{
132132
// Check if we're entering the target method
133-
if ($node instanceof \PhpParser\Node\Stmt\ClassMethod &&
134-
$node->name instanceof Node\Identifier &&
133+
if ($node instanceof \PhpParser\Node\Stmt\ClassMethod &&
134+
$node->name instanceof Node\Identifier &&
135135
$node->name->toString() === $this->method) {
136136
$this->insideTargetMethod = true;
137+
137138
return null;
138139
}
139-
140+
140141
// Only analyze nodes if we're inside the target method
141-
if (!$this->insideTargetMethod) {
142+
if (! $this->insideTargetMethod) {
142143
return null;
143144
}
144-
145+
145146
// Detect response() helper calls
146147
if ($node instanceof MethodCall && $this->isResponseCall($node)) {
147148
$this->analyzer->analyzeResponseCall($node, $this->controller, $this->method);
@@ -184,27 +185,52 @@ public function enterNode(Node $node)
184185

185186
return null;
186187
}
187-
188+
188189
public function leaveNode(Node $node)
189190
{
190191
// Check if we're leaving the target method
191-
if ($node instanceof \PhpParser\Node\Stmt\ClassMethod &&
192-
$node->name instanceof Node\Identifier &&
192+
if ($node instanceof \PhpParser\Node\Stmt\ClassMethod &&
193+
$node->name instanceof Node\Identifier &&
193194
$node->name->toString() === $this->method) {
194195
$this->insideTargetMethod = false;
195196
}
196-
197+
197198
return null;
198199
}
199200

200201
private function isResponseCall(MethodCall $node): bool
201202
{
203+
// Valid response helper methods
204+
$validResponseMethods = [
205+
'json', 'created', 'accepted', 'noContent', 'view',
206+
'redirect', 'redirectTo', 'download', 'file', 'stream',
207+
];
208+
209+
if (! $node->name instanceof Node\Identifier) {
210+
return false;
211+
}
212+
213+
$methodName = $node->name->toString();
214+
215+
// First check if this is a valid response method
216+
if (! in_array($methodName, $validResponseMethods)) {
217+
return false;
218+
}
219+
220+
// Check for direct response() helper chained calls: response()->method()
202221
if ($node->var instanceof FuncCall &&
203222
$node->var->name instanceof Name &&
204223
$node->var->name->toString() === 'response') {
205224
return true;
206225
}
207226

227+
// Check for response() helper variable assignments: $response = response(); $response->method()
228+
if ($node->var instanceof Node\Expr\Variable &&
229+
is_string($node->var->name) &&
230+
$node->var->name === 'response') {
231+
return true;
232+
}
233+
208234
return false;
209235
}
210236

@@ -254,7 +280,7 @@ private function isResourceNew(New_ $node): bool
254280

255281
private function isSetStatusCodeCall(MethodCall $node): bool
256282
{
257-
return $node->name instanceof Node\Identifier &&
283+
return $node->name instanceof Node\Identifier &&
258284
$node->name->toString() === 'setStatusCode';
259285
}
260286
};
@@ -275,25 +301,65 @@ public function analyzeResponseCall(MethodCall $node, string $controller, string
275301

276302
$methodName = $node->name->toString();
277303

304+
// Only process if this is actually a response() helper call, not other method calls
305+
if (! $this->isActualResponseHelperCall($node)) {
306+
return;
307+
}
308+
278309
switch ($methodName) {
279310
case 'json':
280311
$this->analyzeJsonCall($node, $controller, $method);
281312
break;
282313
case 'created':
283-
$this->addSuccessResponse('201', 'Resource created', $controller, $method, 'application/json');
314+
$this->addSuccessResponse('201', 'Resource created', $controller, $method, 'application/json', null, [], null);
284315
break;
285316
case 'accepted':
286-
$this->addSuccessResponse('202', 'Request accepted', $controller, $method, 'application/json');
317+
$this->addSuccessResponse('202', 'Request accepted', $controller, $method, 'application/json', null, [], null);
287318
break;
288319
case 'noContent':
289320
$this->addResponse('204', 'No content', null);
290321
break;
322+
case 'view':
323+
$this->addResponse('200', 'HTML view response', 'text/html');
324+
break;
325+
case 'redirectTo':
326+
case 'redirect':
327+
$this->addResponse('302', 'Redirect response', null);
328+
break;
291329
default:
292330
// Try to extract status from method call
293331
$this->analyzeGenericResponseCall($node);
294332
}
295333
}
296334

335+
/**
336+
* Check if this is actually a response() helper method call
337+
*/
338+
private function isActualResponseHelperCall(MethodCall $node): bool
339+
{
340+
// Valid response helper methods
341+
$validResponseMethods = [
342+
'json', 'created', 'accepted', 'noContent', 'view',
343+
'redirect', 'redirectTo', 'download', 'file', 'stream',
344+
];
345+
346+
if (! $node->name instanceof Node\Identifier) {
347+
return false;
348+
}
349+
350+
$methodName = $node->name->toString();
351+
352+
// Check if this is a valid response method
353+
if (! in_array($methodName, $validResponseMethods)) {
354+
return false;
355+
}
356+
357+
// Check if the call is actually on the response() helper
358+
return $node->var instanceof FuncCall &&
359+
$node->var->name instanceof Name &&
360+
$node->var->name->toString() === 'response';
361+
}
362+
297363
/**
298364
* Analyze response()->json() calls
299365
*/
@@ -319,7 +385,7 @@ public function analyzeJsonCall(MethodCall $node, string $controller, string $me
319385

320386
// Use detailed schema analysis for success responses
321387
if (in_array($statusCode, ['200', '201', '202'])) {
322-
$this->addSuccessResponse($statusCode, $this->getStatusDescription($statusCode), $controller, $method, 'application/json', $data);
388+
$this->addSuccessResponse($statusCode, $this->getStatusDescription($statusCode), $controller, $method, 'application/json', $data, [], null);
323389
} else {
324390
$this->addResponse($statusCode, $this->getStatusDescription($statusCode), 'application/json', $data);
325391
}
@@ -397,14 +463,16 @@ public function analyzeSetStatusCodeCall(MethodCall $node, string $controller, s
397463

398464
// Get response schema from comprehensive analysis
399465
$responseSchema = $this->responseAnalyzer->analyzeControllerMethod($controller, $method);
400-
466+
401467
$this->addSuccessResponse(
402-
$statusCode,
403-
$this->getStatusDescription($statusCode),
404-
$controller,
405-
$method,
468+
$statusCode,
469+
$this->getStatusDescription($statusCode),
470+
$controller,
471+
$method,
406472
'application/json',
407-
$responseSchema
473+
$responseSchema,
474+
[],
475+
null
408476
);
409477
}
410478

@@ -448,7 +516,7 @@ public function analyzeCustomHelperCall(MethodCall $node, string $controller, st
448516

449517
// Use detailed schema analysis for success responses
450518
if (in_array($status, ['200', '201', '202']) && $contentType === 'application/json') {
451-
$this->addSuccessResponse($status, $description, $controller, $method, $contentType);
519+
$this->addSuccessResponse($status, $description, $controller, $method, $contentType, null, [], null);
452520
} else {
453521
$this->addResponse($status, $description, $contentType);
454522
}
@@ -508,7 +576,7 @@ public function analyzeResourceNew(New_ $node, string $controller, string $metho
508576
}
509577

510578
// Use detailed schema analysis for resource responses
511-
$this->addSuccessResponse('200', 'Success', $controller, $method, 'application/json', $analysis);
579+
$this->addSuccessResponse('200', 'Success', $controller, $method, 'application/json', $analysis, [], null);
512580
}
513581
}
514582

@@ -684,7 +752,8 @@ private function processDataResponseAttributes(ReflectionMethod $method): void
684752
$method->getName(),
685753
'application/json',
686754
$schema,
687-
$headers
755+
$headers,
756+
is_string($resource) ? $resource : null
688757
);
689758
} else {
690759
$this->addResponse($statusCode, $description, 'application/json', $schema, $headers);
@@ -1277,7 +1346,7 @@ private function addResponse(string $statusCode, string $description, ?string $c
12771346
/**
12781347
* Add success response with detailed schema analysis
12791348
*/
1280-
private function addSuccessResponse(string $statusCode, string $description, string $controller, string $method, ?string $contentType = 'application/json', ?array $additionalSchema = null, array $headers = []): void
1349+
private function addSuccessResponse(string $statusCode, string $description, string $controller, string $method, ?string $contentType = 'application/json', ?array $additionalSchema = null, array $headers = [], ?string $resourceClass = null): void
12811350
{
12821351
if (! isset($this->detectedResponses[$statusCode])) {
12831352
// Use existing ResponseAnalyzer for detailed schema analysis
@@ -1294,13 +1363,20 @@ private function addSuccessResponse(string $statusCode, string $description, str
12941363
// Generate MediaType-level example from schema property examples
12951364
$responseExample = $this->generateResponseExampleFromSchema($detailedSchema);
12961365

1297-
$this->detectedResponses[$statusCode] = [
1366+
$response = [
12981367
'description' => $description,
12991368
'content_type' => $contentType,
13001369
'headers' => $headers,
13011370
'schema' => $detailedSchema,
13021371
'example' => $responseExample, // Add MediaType-level example
13031372
];
1373+
1374+
// Add resource class if specified (for tests and enhanced documentation)
1375+
if ($resourceClass) {
1376+
$response['resource'] = $resourceClass;
1377+
}
1378+
1379+
$this->detectedResponses[$statusCode] = $response;
13041380
}
13051381
}
13061382

src/Services/OpenApi.php

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -420,16 +420,25 @@ private function processOperationResponses(Operation $operation, array $route, ?
420420
}
421421
}
422422

423-
$responses[$statusCode] = new Response([
424-
'description' => $responseData['description'],
425-
'headers' => $responseHeaders,
426-
'content' => [
427-
$responseData['content_type'] => new MediaType([
428-
'schema' => $schema,
429-
'example' => $this->filterSpatieInternalFields($responseData['example']),
430-
]),
431-
],
432-
]);
423+
// Handle 204 No Content responses or responses with no content
424+
if ($statusCode == 204 || $statusCode === '204' || empty($responseData['content_type']) || $responseData['content_type'] === null || ($statusCode == 204 && ($responseData['type'] === 'null' || $responseData['type'] === null))) {
425+
$responses[$statusCode] = new Response([
426+
'description' => $responseData['description'] ?? ($statusCode === '204' ? 'No content' : ''),
427+
'headers' => $responseHeaders,
428+
// No content property for 204 responses or null/empty content_type
429+
]);
430+
} else {
431+
$responses[$statusCode] = new Response([
432+
'description' => $responseData['description'],
433+
'headers' => $responseHeaders,
434+
'content' => [
435+
$responseData['content_type'] => new MediaType([
436+
'schema' => $schema,
437+
'example' => $this->filterSpatieInternalFields($responseData['example']),
438+
]),
439+
],
440+
]);
441+
}
433442
}
434443
}
435444

@@ -561,13 +570,22 @@ private function processOperationResponses(Operation $operation, array $route, ?
561570
$mediaTypeConfig['example'] = $this->filterSpatieInternalFields($responseData['example']);
562571
}
563572

564-
$responses[$statusCode] = new Response([
565-
'description' => $responseData['description'] ?? '',
566-
'headers' => $responseHeaders,
567-
'content' => [
568-
$responseData['content_type'] ?? 'application/json' => new MediaType($mediaTypeConfig),
569-
],
570-
]);
573+
// Handle 204 No Content responses or responses with no content
574+
if ($statusCode == 204 || $statusCode === '204' || empty($responseData['content_type']) || $responseData['content_type'] === null || ($statusCode == 204 && ($responseData['type'] === 'null' || $responseData['type'] === null))) {
575+
$responses[$statusCode] = new Response([
576+
'description' => $responseData['description'] ?? ($statusCode === '204' ? 'No content' : ''),
577+
'headers' => $responseHeaders,
578+
// No content property for 204 responses or null/empty content_type
579+
]);
580+
} else {
581+
$responses[$statusCode] = new Response([
582+
'description' => $responseData['description'] ?? '',
583+
'headers' => $responseHeaders,
584+
'content' => [
585+
$responseData['content_type'] ?? 'application/json' => new MediaType($mediaTypeConfig),
586+
],
587+
]);
588+
}
571589
}
572590
}
573591

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace JkBennemann\LaravelApiDocumentation\Tests\Stubs\Controllers;
4+
5+
use Illuminate\Http\Response;
6+
7+
class TwoFactorTestController
8+
{
9+
public function enable(): Response
10+
{
11+
return response()
12+
->noContent(204);
13+
}
14+
15+
public function confirm(): Response
16+
{
17+
return response()
18+
->noContent(204);
19+
}
20+
21+
public function login(): Response
22+
{
23+
return response()->json([
24+
'access_token' => 'jwt_token_here',
25+
'user_id' => '123',
26+
], 200);
27+
}
28+
}

0 commit comments

Comments
 (0)