Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
JsonStream
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
4 / 4
5
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%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withPayload
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 jsonEncode
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 * @link      https://github.com/php-fast-forward/http-message
12 * @copyright Copyright (c) 2025 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
13 * @license   https://opensource.org/licenses/MIT MIT License
14 */
15
16namespace FastForward\Http\Message;
17
18use Nyholm\Psr7\Stream;
19
20/**
21 * Class JsonStream.
22 *
23 * Provides a JSON-specific stream implementation, extending Nyholm's PSR-7 Stream.
24 * This class SHALL encapsulate a JSON-encoded payload within an in-memory PHP stream,
25 * while retaining the original decoded payload for convenient retrieval.
26 *
27 * Implementations of this class MUST properly handle JSON encoding errors and SHALL explicitly
28 * prohibit the inclusion of resource types within the JSON payload.
29 *
30 * @package FastForward\Http\Message
31 */
32final class JsonStream extends Stream implements PayloadStreamInterface
33{
34    /**
35     * JSON encoding flags to be applied by default.
36     *
37     * The options JSON_THROW_ON_ERROR, JSON_UNESCAPED_SLASHES, and JSON_UNESCAPED_UNICODE
38     * SHALL be applied to enforce strict error handling and produce readable JSON output.
39     *
40     * @var int
41     */
42    public const ENCODING_OPTIONS = JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
43
44    /**
45     * @var mixed The decoded payload provided to the stream. This MUST be JSON-encodable and MUST NOT contain resources.
46     */
47    private mixed $payload = [];
48
49    /**
50     * @var int the JSON encoding options to be applied
51     */
52    private int $encodingOptions;
53
54    /**
55     * Constructs a new JsonStream instance with the provided payload.
56     *
57     * The payload SHALL be JSON-encoded and written to an in-memory stream. The original payload is retained
58     * in its decoded form for later access via getPayload().
59     *
60     * @param mixed $payload         The data to encode as JSON. MUST be JSON-encodable. Resources are explicitly prohibited.
61     * @param int   $encodingOptions Optional JSON encoding flags. If omitted, ENCODING_OPTIONS will be applied.
62     */
63    public function __construct(mixed $payload = [], int $encodingOptions = self::ENCODING_OPTIONS)
64    {
65        $this->payload         = $payload;
66        $this->encodingOptions = $encodingOptions;
67
68        parent::__construct(fopen('php://temp', 'wb+'));
69
70        $this->write($this->jsonEncode($this->payload, $this->encodingOptions));
71        $this->rewind();
72    }
73
74    /**
75     * Retrieves the decoded payload associated with the stream.
76     *
77     * This method SHALL return the original JSON-encodable payload provided during construction or via withPayload().
78     *
79     * @return mixed the decoded payload
80     */
81    public function getPayload(): mixed
82    {
83        return $this->payload;
84    }
85
86    /**
87     * Returns a new instance of the stream with the specified payload.
88     *
89     * This method MUST return a new JsonStream instance with the body replaced by a stream
90     * containing the JSON-encoded form of the new payload. The current instance SHALL remain unchanged.
91     *
92     * @param mixed $payload the new JSON-encodable payload
93     *
94     * @return self a new JsonStream instance containing the updated payload
95     */
96    public function withPayload(mixed $payload): self
97    {
98        return new self($payload, $this->encodingOptions);
99    }
100
101    /**
102     * Encodes the given data as JSON, enforcing proper error handling.
103     *
104     * If the provided data is a resource, this method SHALL throw an \InvalidArgumentException,
105     * as resource types are not supported by JSON.
106     *
107     * @param mixed $data            the data to encode as JSON
108     * @param int   $encodingOptions JSON encoding options to apply. JSON_THROW_ON_ERROR will always be enforced.
109     *
110     * @return string the JSON-encoded string representation of the data
111     *
112     * @throws \InvalidArgumentException if the data contains a resource
113     * @throws \JsonException            if JSON encoding fails
114     */
115    private function jsonEncode(mixed $data, int $encodingOptions): string
116    {
117        if (\is_resource($data)) {
118            throw new \InvalidArgumentException('Cannot JSON encode resources.');
119        }
120
121        // Reset potential previous errors
122        json_encode(null);
123
124        return json_encode($data, $encodingOptions | JSON_THROW_ON_ERROR);
125    }
126}