Problem Statement
When using #[ResponseBody], the HTTP status code is always taken from the annotation's status parameter (default 200). There's no way to return different status codes from the same method based on logic (e.g., 401 for bad credentials, 200 for success).
Currently, services that need dynamic status codes must either:
- Avoid
#[ResponseBody] and use $this->send() manually
- Include an
http_code field in the JSON body (which doesn't affect the actual HTTP response code)
Proposed Solution
Introduce a ResponseEntity class that wraps the response body, status code, and content type:
class ResponseEntity {
public function __construct(
private mixed $body,
private int $status = 200,
private string $contentType = 'application/json'
) {}
public function getBody(): mixed { return $this->body; }
public function getStatus(): int { return $this->status; }
public function getContentType(): string { return $this->contentType; }
public static function ok(mixed $body): self { return new self($body, 200); }
public static function notFound(mixed $body): self { return new self($body, 404); }
public static function unauthorized(mixed $body): self { return new self($body, 401); }
public static function forbidden(mixed $body): self { return new self($body, 403); }
public static function error(mixed $body): self { return new self($body, 500); }
}
When handleMethodResponse() detects the return value is a ResponseEntity, it uses its status and content type instead of the annotation defaults:
if ($result instanceof ResponseEntity) {
$this->send($result->getContentType(), $result->getBody(), $result->getStatus());
return;
}
// ... existing handling for Json/array/string
Usage
#[PostMapping]
#[ResponseBody]
#[AllowAnonymous]
public function login(string $username, string $password): ResponseEntity {
if ($loginInfo === null) {
return ResponseEntity::unauthorized(new Json(['message' => 'Incorrect credentials']));
}
return ResponseEntity::ok(new Json(['status' => 'success', 'jwt' => $token]));
}
Alternatives Considered
-
statusKey parameter on #[ResponseBody] — e.g., #[ResponseBody(statusKey: 'http_code')] to read status from the returned Json/array/object. Works but relies on magic key names and doesn't work for string/scalar returns.
-
Not using #[ResponseBody] — use $this->send() manually. Works but loses the benefits of auto-processing (cleaner method signatures, return-value-based responses).
Breaking Change
No — this is purely additive. Existing code returning Json, arrays, or strings continues to work unchanged. ResponseEntity is opt-in.
Additional Context
This was discovered while building auth services where the same endpoint needs to return 200 (success), 401 (bad credentials), or 403 (no portal access) depending on the logic path.
Problem Statement
When using
#[ResponseBody], the HTTP status code is always taken from the annotation'sstatusparameter (default 200). There's no way to return different status codes from the same method based on logic (e.g., 401 for bad credentials, 200 for success).Currently, services that need dynamic status codes must either:
#[ResponseBody]and use$this->send()manuallyhttp_codefield in the JSON body (which doesn't affect the actual HTTP response code)Proposed Solution
Introduce a
ResponseEntityclass that wraps the response body, status code, and content type:When
handleMethodResponse()detects the return value is aResponseEntity, it uses its status and content type instead of the annotation defaults:Usage
Alternatives Considered
statusKeyparameter on#[ResponseBody]— e.g.,#[ResponseBody(statusKey: 'http_code')]to read status from the returned Json/array/object. Works but relies on magic key names and doesn't work for string/scalar returns.Not using
#[ResponseBody]— use$this->send()manually. Works but loses the benefits of auto-processing (cleaner method signatures, return-value-based responses).Breaking Change
No — this is purely additive. Existing code returning
Json, arrays, or strings continues to work unchanged.ResponseEntityis opt-in.Additional Context
This was discovered while building auth services where the same endpoint needs to return 200 (success), 401 (bad credentials), or 403 (no portal access) depending on the logic path.