Skip to content

Commit a32526f

Browse files
devanychsamdark
andauthored
Fix overwriting of stream content, add exception throwing, add more tests (#57)
Co-authored-by: Alexander Makarov <[email protected]>
1 parent 2c36a5d commit a32526f

6 files changed

+346
-29
lines changed

src/DataResponse.php

+85-25
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66

77
use Psr\Http\Message\ResponseFactoryInterface;
88
use Psr\Http\Message\ResponseInterface;
9+
use Psr\Http\Message\StreamFactoryInterface;
910
use Psr\Http\Message\StreamInterface;
1011
use RuntimeException;
1112

13+
use function ftruncate;
1214
use function get_class;
1315
use function gettype;
1416
use function is_callable;
1517
use function is_object;
18+
use function is_resource;
1619
use function is_string;
20+
use function rewind;
1721
use function sprintf;
1822

1923
/**
@@ -28,7 +32,14 @@ final class DataResponse implements ResponseInterface
2832
* @var mixed
2933
*/
3034
private $data;
35+
36+
/**
37+
* @var resource
38+
*/
39+
private $resource;
40+
3141
private bool $formatted = false;
42+
private bool $forcedBody = false;
3243
private ResponseInterface $response;
3344
private ?StreamInterface $dataStream = null;
3445
private ?DataResponseFormatterInterface $responseFormatter = null;
@@ -38,11 +49,17 @@ final class DataResponse implements ResponseInterface
3849
* @param int $code The response status code.
3950
* @param string $reasonPhrase The response reason phrase associated with the status code.
4051
* @param ResponseFactoryInterface $responseFactory The response factory instance.
52+
* @param StreamFactoryInterface $streamFactory The stream factory instance.
4153
*/
42-
public function __construct($data, int $code, string $reasonPhrase, ResponseFactoryInterface $responseFactory)
43-
{
44-
$this->response = $responseFactory->createResponse($code, $reasonPhrase);
54+
public function __construct(
55+
$data,
56+
int $code,
57+
string $reasonPhrase,
58+
ResponseFactoryInterface $responseFactory,
59+
StreamFactoryInterface $streamFactory
60+
) {
4561
$this->data = $data;
62+
$this->createResponse($code, $reasonPhrase, $responseFactory, $streamFactory);
4663
}
4764

4865
public function getBody(): StreamInterface
@@ -52,18 +69,20 @@ public function getBody(): StreamInterface
5269
}
5370

5471
if ($this->hasResponseFormatter()) {
55-
$this->response = $this->formatResponse();
72+
$this->formatResponse();
5673
return $this->dataStream = $this->response->getBody();
5774
}
5875

5976
if ($this->data === null) {
77+
$this->clearResponseBody();
6078
return $this->dataStream = $this->response->getBody();
6179
}
6280

6381
/** @var mixed */
6482
$data = $this->getData();
6583

6684
if (is_string($data)) {
85+
$this->clearResponseBody();
6786
$this->response->getBody()->write($data);
6887
return $this->dataStream = $this->response->getBody();
6988
}
@@ -84,7 +103,7 @@ public function getBody(): StreamInterface
84103
*/
85104
public function getHeader($name): array
86105
{
87-
$this->response = $this->formatResponse();
106+
$this->formatResponse();
88107
return $this->response->getHeader($name);
89108
}
90109

@@ -95,7 +114,7 @@ public function getHeader($name): array
95114
*/
96115
public function getHeaderLine($name): string
97116
{
98-
$this->response = $this->formatResponse();
117+
$this->formatResponse();
99118
return $this->response->getHeaderLine($name);
100119
}
101120

@@ -106,25 +125,25 @@ public function getHeaderLine($name): string
106125
*/
107126
public function getHeaders(): array
108127
{
109-
$this->response = $this->formatResponse();
128+
$this->formatResponse();
110129
return $this->response->getHeaders();
111130
}
112131

113132
public function getProtocolVersion(): string
114133
{
115-
$this->response = $this->formatResponse();
134+
$this->formatResponse();
116135
return $this->response->getProtocolVersion();
117136
}
118137

119138
public function getReasonPhrase(): string
120139
{
121-
$this->response = $this->formatResponse();
140+
$this->formatResponse();
122141
return $this->response->getReasonPhrase();
123142
}
124143

125144
public function getStatusCode(): int
126145
{
127-
$this->response = $this->formatResponse();
146+
$this->formatResponse();
128147
return $this->response->getStatusCode();
129148
}
130149

@@ -135,7 +154,7 @@ public function getStatusCode(): int
135154
*/
136155
public function hasHeader($name): bool
137156
{
138-
$this->response = $this->formatResponse();
157+
$this->formatResponse();
139158
return $this->response->hasHeader($name);
140159
}
141160

@@ -165,6 +184,7 @@ public function withBody(StreamInterface $body): self
165184
$new = clone $this;
166185
$new->response = $this->response->withBody($body);
167186
$new->dataStream = $body;
187+
$new->forcedBody = true;
168188
$new->formatted = false;
169189
return $new;
170190
}
@@ -195,7 +215,7 @@ public function withHeader($name, $value): self
195215
public function withoutHeader($name): self
196216
{
197217
$new = clone $this;
198-
$new->response = $new->formatResponse();
218+
$new->formatResponse();
199219
$new->response = $new->response->withoutHeader($name);
200220
return $new;
201221
}
@@ -267,12 +287,23 @@ public function hasResponseFormatter(): bool
267287
*
268288
* @param mixed $data The response data.
269289
*
290+
* @throws RuntimeException If the body was previously forced to be set {@see withBody()}.
291+
*
270292
* @return self
271293
*/
272294
public function withData($data): self
273295
{
296+
if ($this->forcedBody) {
297+
throw new RuntimeException(sprintf(
298+
'The data cannot be set because the body was previously'
299+
. ' forced to be set using the "%s::withBody()" method.',
300+
self::class,
301+
));
302+
}
303+
274304
$new = clone $this;
275305
$new->data = $data;
306+
$new->dataStream = null;
276307
$new->formatted = false;
277308
return $new;
278309
}
@@ -307,13 +338,11 @@ public function hasData(): bool
307338

308339
/**
309340
* Formats the response, if necessary.
310-
*
311-
* @return ResponseInterface Formatted response.
312341
*/
313-
private function formatResponse(): ResponseInterface
342+
private function formatResponse(): void
314343
{
315-
if (!$this->needFormatResponse()) {
316-
return $this->response;
344+
if ($this->formatted || !$this->hasResponseFormatter()) {
345+
return;
317346
}
318347

319348
/** @psalm-var DataResponseFormatterInterface $this->responseFormatter */
@@ -330,25 +359,56 @@ private function formatResponse(): ResponseInterface
330359
));
331360
}
332361

333-
return $response;
362+
$this->response = $response;
334363
}
335364

336365
/**
337366
* Clears a response body.
338367
*/
339368
private function clearResponseBody(): void
340369
{
341-
$this->response->getBody()->rewind();
342-
$this->response->getBody()->write('');
370+
if (!$this->forcedBody) {
371+
ftruncate($this->resource, 0);
372+
rewind($this->resource);
373+
}
343374
}
344375

345376
/**
346-
* Checks whether the response needs to be formatted.
377+
* Creates a new response by retrieving and validating the stream resource.
347378
*
348-
* @return bool Whether the response needs to be formatted.
379+
* @param int $code The response status code.
380+
* @param string $reasonPhrase The response reason phrase associated with the status code.
381+
* @param ResponseFactoryInterface $responseFactory The response factory instance.
382+
* @param StreamFactoryInterface $streamFactory The stream factory instance.
383+
*
384+
* @throws RuntimeException If the stream resource is not valid.
349385
*/
350-
private function needFormatResponse(): bool
351-
{
352-
return $this->formatted === false && $this->hasResponseFormatter();
386+
private function createResponse(
387+
int $code,
388+
string $reasonPhrase,
389+
ResponseFactoryInterface $responseFactory,
390+
StreamFactoryInterface $streamFactory
391+
): void {
392+
$response = $responseFactory->createResponse($code, $reasonPhrase);
393+
$stream = $response->getBody();
394+
395+
if (!$stream->isReadable()) {
396+
throw new RuntimeException('Stream is not readable.');
397+
}
398+
399+
if (!$stream->isSeekable()) {
400+
throw new RuntimeException('Stream is not seekable.');
401+
}
402+
403+
if (!$stream->isWritable()) {
404+
throw new RuntimeException('Stream is not writable.');
405+
}
406+
407+
if (!is_resource($resource = $stream->detach())) {
408+
throw new RuntimeException('Resource was not separated from the stream.');
409+
}
410+
411+
$this->resource = $resource;
412+
$this->response = $response->withBody($streamFactory->createStreamFromResource($this->resource));
353413
}
354414
}

src/DataResponseFactory.php

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Yiisoft\DataResponse;
66

77
use Psr\Http\Message\ResponseFactoryInterface;
8+
use Psr\Http\Message\StreamFactoryInterface;
89
use Yiisoft\Http\Status;
910

1011
/**
@@ -13,14 +14,16 @@
1314
final class DataResponseFactory implements DataResponseFactoryInterface
1415
{
1516
private ResponseFactoryInterface $responseFactory;
17+
private StreamFactoryInterface $streamFactory;
1618

17-
public function __construct(ResponseFactoryInterface $responseFactory)
19+
public function __construct(ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory)
1820
{
1921
$this->responseFactory = $responseFactory;
22+
$this->streamFactory = $streamFactory;
2023
}
2124

2225
public function createResponse($data = null, int $code = Status::OK, string $reasonPhrase = ''): DataResponse
2326
{
24-
return new DataResponse($data, $code, $reasonPhrase, $this->responseFactory);
27+
return new DataResponse($data, $code, $reasonPhrase, $this->responseFactory, $this->streamFactory);
2528
}
2629
}

0 commit comments

Comments
 (0)