Skip to content

Commit 28ef5fd

Browse files
committed
Symfony stream json factory improvement (accept iterable insted of Iterator)
1 parent e36a72e commit 28ef5fd

File tree

6 files changed

+116
-14
lines changed

6 files changed

+116
-14
lines changed

.github/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ $response = \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJson($y
125125
// response with some array or collection (avoiding out of memory problem recommended some lazy loading iterator)
126126
$response = \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJsonStream(new \ArrayIterator([$yourModelForResponseBody]), $transformer);
127127
//$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJsonStream(new \ArrayIterator([$yourModelForResponseBody]), $transformer);
128+
//$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJsonStream([$yourModelForResponseBody], $transformer);
128129

129130
```
130131

src/Factory/Server/ResponseFactory.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Kayex\HttpCodes;
1111
use Psr\Http\Message\ResponseFactoryInterface;
1212
use Psr\Http\Message\ResponseInterface;
13+
use SimpleAsFuck\ApiToolkit\Service\Server\SpeedLimitService;
1314
use SimpleAsFuck\ApiToolkit\Service\Transformation\Transformer;
1415

1516
final class ResponseFactory
@@ -72,13 +73,7 @@ public static function makeJsonStream(\Iterator $body, ?Transformer $transformer
7273
$body->next();
7374
if ($body->valid()) {
7475
$item .= ',';
75-
if ($speedLimit > 0) {
76-
$timeLimit = (strlen($item) / 1024) / $speedLimit;
77-
$timeOverHead = $timeLimit - (\microtime(true) - $previousItemTime);
78-
if ($timeOverHead > 0) {
79-
\usleep((int) ($timeOverHead * 1000000));
80-
}
81-
}
76+
SpeedLimitService::slowdownDataSending($speedLimit, strlen($item), $previousItemTime);
8277
} else {
8378
$item .= ']';
8479
}

src/Factory/Symfony/ResponseFactory.php

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
namespace SimpleAsFuck\ApiToolkit\Factory\Symfony;
66

7+
use GuzzleHttp\Utils;
78
use Kayex\HttpCodes;
9+
use SimpleAsFuck\ApiToolkit\Service\Server\SpeedLimitService;
810
use SimpleAsFuck\ApiToolkit\Service\Transformation\Transformer;
911
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
12+
use Symfony\Component\HttpFoundation\StreamedResponse;
1013

1114
final class ResponseFactory
1215
{
@@ -25,15 +28,40 @@ public static function makeJson($body, ?Transformer $transformer = null, int $co
2528

2629
/**
2730
* @template TBody
28-
* @param \Iterator<TBody> $body
31+
* @param iterable<TBody> $body
2932
* @param Transformer<TBody>|null $transformer
3033
* @param int<100,505> $code
3134
* @param array<non-empty-string, string|array<string>> $headers
3235
* @param float $speedLimit speed limit in KB/s for sending response slow down, zero means no slow down (this is not precise but is something)
3336
*/
34-
public static function makeJsonStream(\Iterator $body, ?Transformer $transformer = null, int $code = HttpCodes::HTTP_OK, array $headers = [], float $speedLimit = 0): \Symfony\Component\HttpFoundation\Response
37+
public static function makeJsonStream(iterable $body, ?Transformer $transformer = null, int $code = HttpCodes::HTTP_OK, array $headers = [], float $speedLimit = 0): StreamedResponse
3538
{
36-
$factory = new HttpFoundationFactory();
37-
return $factory->createResponse(\SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJsonStream($body, $transformer, $code, $headers, $speedLimit), true);
39+
return new StreamedResponse(
40+
static function () use ($body, $transformer, $speedLimit) {
41+
$dataSeparator = '';
42+
43+
echo '[';
44+
45+
foreach ($body as $responseData) {
46+
$batchSendingStartAt = \microtime(true);
47+
echo $dataSeparator;
48+
49+
if ($transformer !== null) {
50+
$responseData = $transformer->toApi($responseData);
51+
}
52+
53+
/** @var non-empty-string $responseData */
54+
$responseData = Utils::jsonEncode($responseData);
55+
echo $responseData;
56+
57+
SpeedLimitService::slowdownDataSending($speedLimit, strlen($dataSeparator) + strlen($responseData), $batchSendingStartAt);
58+
$dataSeparator = ',';
59+
}
60+
61+
echo ']';
62+
},
63+
$code,
64+
$headers
65+
);
3866
}
3967
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleAsFuck\ApiToolkit\Service\Server;
6+
7+
final class SpeedLimitService
8+
{
9+
/**
10+
* @param float $speedLimit speed limit in KB/s for sending response slow down, zero means no slow down (this is not precise but is something)
11+
* @param positive-int $batchSize size of sended batch in bytes
12+
* @param float $batchSendingStartAt microtime when sending the batch started
13+
*/
14+
public static function slowdownDataSending(float $speedLimit, int $batchSize, float $batchSendingStartAt): void
15+
{
16+
if ($speedLimit > 0) {
17+
$timeLimit = ($batchSize / 1024) / $speedLimit;
18+
$timeOverHead = $timeLimit - (\microtime(true) - $batchSendingStartAt);
19+
if ($timeOverHead > 0) {
20+
\usleep((int) ($timeOverHead * 1000000));
21+
}
22+
}
23+
}
24+
}

test/Factory/Server/ResponseFactoryTest.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,26 @@
77

88
final class ResponseFactoryTest extends TestCase
99
{
10-
public function testMakeJsonStream(): void
10+
/**
11+
* @dataProvider dataProviderMakeJsonStream
12+
*
13+
* @param array<mixed> $streamedData
14+
*/
15+
public function testMakeJsonStream(string $expectedBody, array $streamedData): void
1116
{
12-
$response = ResponseFactory::makeJsonStream(new \ArrayIterator([548846, 'sadasjkfghjsg']));
17+
$response = ResponseFactory::makeJsonStream(new \ArrayIterator($streamedData));
1318

14-
self::assertSame('[548846,"sadasjkfghjsg"]', $response->getBody()->getContents());
19+
self::assertSame($expectedBody, $response->getBody()->getContents());
20+
}
21+
22+
/**
23+
* @return array<array<mixed>>
24+
*/
25+
public function dataProviderMakeJsonStream(): array
26+
{
27+
return [
28+
['[548846,"sadasjkfghjsg"]', [548846, 'sadasjkfghjsg']],
29+
['[]', []],
30+
];
1531
}
1632
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Test\Factory\Symfony;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory;
9+
10+
final class ResponseFactoryTest extends TestCase
11+
{
12+
/**
13+
* @dataProvider dataProviderMakeJsonStream
14+
*
15+
* @param iterable<mixed> $streamedData
16+
*/
17+
public function testMakeJsonStream(string $expectedBody, iterable $streamedData): void
18+
{
19+
$response = ResponseFactory::makeJsonStream($streamedData);
20+
21+
ob_start();
22+
$response->sendContent();
23+
$responseContent = ob_get_clean();
24+
25+
self::assertSame($expectedBody, $responseContent);
26+
}
27+
28+
/**
29+
* @return array<array<mixed>>
30+
*/
31+
public function dataProviderMakeJsonStream(): array
32+
{
33+
return [
34+
['[548846,"sadasjkfghjsg"]', [548846, 'sadasjkfghjsg']],
35+
['[]', new \ArrayIterator([])],
36+
];
37+
}
38+
}

0 commit comments

Comments
 (0)