Skip to content

feat: Add ResponseEntity class for dynamic HTTP status codes with #[ResponseBody] #107

@usernane

Description

@usernane

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

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions