Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
JsonResponse
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
3 / 3
7
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getPayload
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 withPayload
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of php-fast-forward/http-message.
7 *
8 * This source file is subject to the license bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2025-2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/http-message
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\Http\Message;
20
21use FastForward\Http\Message\Header\ContentType;
22use JsonException;
23use Nyholm\Psr7\Response;
24use RuntimeException;
25
26/**
27 * Class JsonResponse.
28 *
29 * Provides a JSON-specific HTTP response implementation that complies with PSR-7 Response interfaces.
30 * This class MUST be used when returning JSON payloads over HTTP responses.
31 * It automatically sets the 'Content-Type' header to 'application/json' with the specified charset.
32 *
33 * A body replaced through PSR-7 withBody() may be any StreamInterface.
34 */
35 class JsonResponse extends Response implements PayloadResponseInterface
36{
37    /**
38     * Constructs a new JsonResponse instance with an optional payload and charset.
39     *
40     * This constructor SHALL initialize the response body with a JsonStream containing the provided payload.
41     * The 'Content-Type' header MUST be set to 'application/json' with the specified charset.
42     *
43     * @param mixed $payload The JSON-serializable payload to send in the response body. Defaults to an empty array.
44     * @param string $charset The character encoding to use in the 'Content-Type' header. Defaults to 'utf-8'.
45     * @param array $headers
46     */
47    public function __construct(mixed $payload = [], string $charset = 'utf-8', array $headers = [])
48    {
49        $headers['Content-Type'] = ContentType::ApplicationJson->withCharset($charset);
50
51        parent::__construct(
52            headers: $headers,
53            body: new JsonStream($payload),
54        );
55    }
56
57    /**
58     * Retrieves the payload contained in the response body.
59     *
60     * Original payload streams return their stored payload. Generic readable,
61     * seekable streams are decoded as associative JSON without changing the cursor.
62     * Invalid JSON throws JsonException; unreadable, detached or non-seekable
63     * generic streams throw RuntimeException rather than being consumed.
64     *
65     * @return mixed the decoded JSON payload
66     *
67     * @throws JsonException
68     * @throws RuntimeException
69     */
70    public function getPayload(): mixed
71    {
72        $body = $this->getBody();
73        if ($body instanceof PayloadStreamInterface) {
74            return $body->getPayload();
75        }
76
77        if (! $body->isReadable() || ! $body->isSeekable()) {
78            throw new RuntimeException('A generic JSON body must be readable and seekable to retrieve its payload.');
79        }
80
81        $position = $body->tell();
82        try {
83            $body->rewind();
84
85            return json_decode($body->getContents(), true, 512, \JSON_THROW_ON_ERROR);
86        } finally {
87            $body->seek($position);
88        }
89    }
90
91    /**
92     * Returns an instance with the specified payload.
93     *
94     * This method SHALL return a new instance of the response with the body replaced by a new JsonStream
95     * containing the provided payload. The original instance MUST remain unchanged, ensuring immutability
96     * as required by PSR-7.
97     *
98     * @param mixed $payload the new JSON-serializable payload
99     *
100     * @return self a new JsonResponse instance with the updated payload
101     */
102    public function withPayload(mixed $payload): self
103    {
104        $body = $this->getBody();
105
106        return $this->withBody(
107            $body instanceof PayloadStreamInterface ? $body->withPayload($payload) : new JsonStream($payload)
108        );
109    }
110}