Skip to content

Commit 10dfe2f

Browse files
committed
Add JsonStream
1 parent b2f33ba commit 10dfe2f

File tree

2 files changed

+169
-0
lines changed

2 files changed

+169
-0
lines changed

src/JsonStream.php

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
namespace FastForward\Http\Message;
4+
5+
use Nyholm\Psr7\Stream;
6+
7+
/**
8+
* Class JsonStream
9+
*
10+
* Extends Nyholm's PSR-7 Stream implementation to provide JSON-specific stream functionality.
11+
* This class SHALL encapsulate a JSON-encoded payload within a PHP stream, while retaining the original
12+
* payload in a decoded form for convenient access.
13+
*
14+
* Implementations MUST properly handle JSON encoding errors and enforce the prohibition of resource types
15+
* within JSON payloads.
16+
*
17+
* @package FastForward\Http\Message
18+
*/
19+
final class JsonStream extends Stream implements JsonStreamInterface
20+
{
21+
/**
22+
* @var resource The underlying stream resource containing the JSON-encoded data.
23+
* This resource MUST be a writable PHP stream.
24+
*/
25+
private $stream;
26+
27+
/**
28+
* @var mixed The original payload data prior to JSON encoding.
29+
* This MAY be any JSON-encodable PHP type, excluding resources.
30+
*/
31+
private mixed $payload;
32+
33+
/**
34+
* Constructs a new JsonStream instance with the provided payload.
35+
*
36+
* The payload SHALL be JSON-encoded and written to an in-memory PHP stream. The original payload is retained
37+
* in decoded form for later retrieval via {@see getPayload()}.
38+
*
39+
* @param mixed $payload The data to encode as JSON. MUST be JSON-encodable. Resources are explicitly prohibited.
40+
* @param int $encodingOptions Optional JSON encoding flags as defined by {@see json_encode()}. Defaults to 0.
41+
*/
42+
public function __construct(mixed $payload, private int $encodingOptions = 0)
43+
{
44+
$this->setPayload($payload);
45+
46+
parent::__construct($this->stream);
47+
}
48+
49+
/**
50+
* Encodes the given data as JSON, enforcing proper error handling.
51+
*
52+
* If the provided data is a resource, this method SHALL throw an {@see \InvalidArgumentException} as resources
53+
* cannot be represented in JSON format.
54+
*
55+
* @param mixed $data The data to encode as JSON.
56+
* @param int $encodingOptions JSON encoding options, combined with JSON_THROW_ON_ERROR.
57+
* @return string The JSON-encoded string representation of the data.
58+
*
59+
* @throws \InvalidArgumentException If the data contains a resource.
60+
* @throws \JsonException If JSON encoding fails.
61+
*/
62+
private function jsonEncode(mixed $data, int $encodingOptions): string
63+
{
64+
if (is_resource($data)) {
65+
throw new \InvalidArgumentException('Cannot JSON encode resources');
66+
}
67+
68+
// Clear json_last_error()
69+
json_encode(null);
70+
71+
return json_encode($data, $encodingOptions | JSON_THROW_ON_ERROR);
72+
}
73+
74+
/**
75+
* Sets the payload and updates the underlying stream with its JSON-encoded form.
76+
*
77+
* This method SHALL encode the payload as JSON, write it to a temporary stream, and rewind the stream pointer.
78+
* It also retains the original decoded payload for later access.
79+
*
80+
* @param mixed $data The data to encode as JSON. MUST be JSON-encodable.
81+
* @return self The current instance for fluent chaining.
82+
*
83+
* @throws \InvalidArgumentException If the data contains a resource.
84+
* @throws \JsonException If JSON encoding fails.
85+
*/
86+
private function setPayload(mixed $data): self
87+
{
88+
$contents = $this->jsonEncode($data, $this->encodingOptions);
89+
90+
$this->payload = $data;
91+
$this->stream = fopen('php://temp', 'wb+');
92+
93+
$this->write($contents);
94+
$this->rewind();
95+
96+
return $this;
97+
}
98+
99+
/**
100+
* Retrieves the decoded payload previously provided to the stream.
101+
*
102+
* @return mixed The original decoded payload, which MAY be of any JSON-encodable PHP type.
103+
*/
104+
public function getPayload(): mixed
105+
{
106+
return $this->payload;
107+
}
108+
109+
/**
110+
* Returns a new instance with the updated payload encoded as JSON.
111+
*
112+
* This method SHALL NOT modify the current instance. It MUST return a cloned instance with the new payload
113+
* written to its stream.
114+
*
115+
* @param mixed $data The new data to encode as JSON. MUST be JSON-encodable.
116+
* @return self A new instance with the updated JSON payload.
117+
*
118+
* @throws \InvalidArgumentException If the data contains a resource.
119+
* @throws \JsonException If JSON encoding fails.
120+
*/
121+
public function withPayload(mixed $data): self
122+
{
123+
return (clone $this)->setPayload($data);
124+
}
125+
}

src/JsonStreamInterface.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace FastForward\Http\Message;
4+
5+
use Psr\Http\Message\StreamInterface;
6+
7+
/**
8+
* Interface JsonStreamInterface
9+
*
10+
* Extends the PSR-7 StreamInterface to provide additional functionality for JSON payload handling.
11+
* Implementations of this interface MUST support both standard stream operations and structured JSON payload manipulation.
12+
*
13+
* @package FastForward\Http\Message
14+
*/
15+
interface JsonStreamInterface extends StreamInterface
16+
{
17+
/**
18+
* Retrieves the decoded JSON payload from the stream.
19+
*
20+
* This method MUST return the decoded JSON payload as a native PHP type. The returned type MAY vary depending on
21+
* the structure of the JSON content (e.g., array, object, int, float, string, bool, or null).
22+
*
23+
* If the stream does not contain valid JSON, this method SHOULD return null or throw an appropriate exception
24+
* as defined by the implementation.
25+
*
26+
* @return mixed The decoded JSON payload, which MAY be of any type, including array, object, scalar, or null.
27+
*/
28+
public function getPayload(): mixed;
29+
30+
/**
31+
* Returns a new instance with the provided payload encoded as JSON.
32+
*
33+
* This method MUST NOT modify the existing instance; instead, it SHALL return a new instance with the updated
34+
* JSON payload written to the underlying stream.
35+
*
36+
* The provided data MUST be JSON-encodable. If encoding fails, the method MAY throw an exception as defined
37+
* by the implementation.
38+
*
39+
* @param mixed $data The data to encode as JSON and set as the stream's payload. This MAY be of any type
40+
* supported by json_encode.
41+
* @return self A new instance with the updated JSON payload.
42+
*/
43+
public function withPayload(mixed $data): self;
44+
}

0 commit comments

Comments
 (0)