Skip to content

Commit 46d813f

Browse files
authored
[Swoole] Binary file and streamed response support (#97)
* Decouple Symfony response of Swoole * Decouple Symfony request of Swoole * Handle binary and streamed symfony response in Swoole * Apply php-cs-fixer
1 parent 6ef1e17 commit 46d813f

File tree

6 files changed

+201
-63
lines changed

6 files changed

+201
-63
lines changed

src/LaravelRunner.php

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Illuminate\Http\Request as LaravelRequest;
77
use Swoole\Http\Request;
88
use Swoole\Http\Response;
9-
use Symfony\Component\HttpFoundation\HeaderBag;
109
use Symfony\Component\Runtime\RunnerInterface;
1110

1211
/**
@@ -36,27 +35,10 @@ public function run(): int
3635

3736
public function handle(Request $request, Response $response): void
3837
{
39-
// convert to HttpFoundation request
40-
$sfRequest = new LaravelRequest(
41-
$request->get ?? [],
42-
$request->post ?? [],
43-
[],
44-
$request->cookie ?? [],
45-
$request->files ?? [],
46-
array_change_key_case($request->server ?? [], CASE_UPPER),
47-
$request->rawContent()
48-
);
49-
$sfRequest->headers = new HeaderBag($request->header);
38+
$sfRequest = LaravelRequest::createFromBase(SymfonyHttpBridge::convertSwooleRequest($request));
5039

5140
$sfResponse = $this->application->handle($sfRequest);
52-
foreach ($sfResponse->headers->all() as $name => $values) {
53-
foreach ($values as $value) {
54-
$response->header($name, $value);
55-
}
56-
}
57-
58-
$response->status($sfResponse->getStatusCode());
59-
$response->end($sfResponse->getContent());
41+
SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response);
6042

6143
$this->application->terminate($sfRequest, $sfResponse);
6244
}

src/SymfonyHttpBridge.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace Runtime\Swoole;
4+
5+
use Swoole\Http\Request;
6+
use Swoole\Http\Response;
7+
use Symfony\Component\HttpFoundation\BinaryFileResponse;
8+
use Symfony\Component\HttpFoundation\HeaderBag;
9+
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
10+
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
11+
use Symfony\Component\HttpFoundation\StreamedResponse;
12+
13+
/**
14+
* Bridge between Symfony and Swoole Http API.
15+
*
16+
* @author Piotr Kugla <[email protected]>
17+
*
18+
* @internal
19+
*/
20+
final class SymfonyHttpBridge
21+
{
22+
public static function convertSwooleRequest(Request $request): SymfonyRequest
23+
{
24+
$sfRequest = new SymfonyRequest(
25+
$request->get ?? [],
26+
$request->post ?? [],
27+
[],
28+
$request->cookie ?? [],
29+
$request->files ?? [],
30+
array_change_key_case($request->server ?? [], CASE_UPPER),
31+
$request->rawContent()
32+
);
33+
$sfRequest->headers = new HeaderBag($request->header ?? []);
34+
35+
return $sfRequest;
36+
}
37+
38+
public static function reflectSymfonyResponse(SymfonyResponse $sfResponse, Response $response): void
39+
{
40+
foreach ($sfResponse->headers->all() as $name => $values) {
41+
foreach ($values as $value) {
42+
$response->header($name, $value);
43+
}
44+
}
45+
46+
$response->status($sfResponse->getStatusCode());
47+
48+
switch (true) {
49+
case $sfResponse instanceof BinaryFileResponse && $sfResponse->headers->has('Content-Range'):
50+
case $sfResponse instanceof StreamedResponse:
51+
ob_start(function ($buffer) use ($response) {
52+
$response->write($buffer);
53+
54+
return '';
55+
});
56+
$sfResponse->sendContent();
57+
ob_end_clean();
58+
$response->end();
59+
break;
60+
case $sfResponse instanceof BinaryFileResponse:
61+
$response->sendfile($sfResponse->getFile()->getPathname());
62+
break;
63+
default:
64+
$response->end($sfResponse->getContent());
65+
}
66+
}
67+
}

src/SymfonyRunner.php

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
use Swoole\Http\Request;
66
use Swoole\Http\Response;
7-
use Symfony\Component\HttpFoundation\HeaderBag;
8-
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
97
use Symfony\Component\HttpKernel\HttpKernelInterface;
108
use Symfony\Component\HttpKernel\TerminableInterface;
119
use Symfony\Component\Runtime\RunnerInterface;
@@ -37,27 +35,10 @@ public function run(): int
3735

3836
public function handle(Request $request, Response $response): void
3937
{
40-
// convert to HttpFoundation request
41-
$sfRequest = new SymfonyRequest(
42-
$request->get ?? [],
43-
$request->post ?? [],
44-
[],
45-
$request->cookie ?? [],
46-
$request->files ?? [],
47-
array_change_key_case($request->server ?? [], CASE_UPPER),
48-
$request->rawContent()
49-
);
50-
$sfRequest->headers = new HeaderBag($request->header);
38+
$sfRequest = SymfonyHttpBridge::convertSwooleRequest($request);
5139

5240
$sfResponse = $this->application->handle($sfRequest);
53-
foreach ($sfResponse->headers->all() as $name => $values) {
54-
foreach ($values as $value) {
55-
$response->header($name, $value);
56-
}
57-
}
58-
59-
$response->status($sfResponse->getStatusCode());
60-
$response->end($sfResponse->getContent());
41+
SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response);
6142

6243
if ($this->application instanceof TerminableInterface) {
6344
$this->application->terminate($sfRequest, $sfResponse);

tests/Unit/LaravelRunnerTest.php

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use Swoole\Http\Request;
1212
use Swoole\Http\Response;
1313
use Swoole\Http\Server;
14-
use Symfony\Component\HttpFoundation\HeaderBag;
14+
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
1515

1616
class LaravelRunnerTest extends TestCase
1717
{
@@ -32,22 +32,15 @@ public function testRun(): void
3232

3333
public function testHandle(): void
3434
{
35-
$sfResponse = $this->createMock(\Symfony\Component\HttpFoundation\Response::class);
36-
$sfResponse->headers = new HeaderBag(['X-Test' => 'Swoole-Runtime']);
37-
$sfResponse->expects(self::once())->method('getStatusCode')->willReturn(201);
38-
$sfResponse->expects(self::once())->method('getContent')->willReturn('Test');
35+
$sfResponse = new SymfonyResponse('foo');
3936

4037
$application = $this->createMock(Kernel::class);
4138
$application->expects(self::once())->method('handle')->willReturn($sfResponse);
4239

43-
$request = $this->createMock(Request::class);
44-
$request->header = [];
45-
4640
$response = $this->createMock(Response::class);
47-
$response->expects(self::once())->method('header')->with('x-test', 'Swoole-Runtime');
48-
$response->expects(self::once())->method('status')->with(201);
49-
$response->expects(self::once())->method('end')->with('Test');
41+
$response->expects(self::once())->method('end')->with('foo');
5042

43+
$request = $this->createMock(Request::class);
5144
$factory = $this->createMock(ServerFactory::class);
5245

5346
$runner = new LaravelRunner($factory, $application);

tests/Unit/SymfonyHttpBridgeTest.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
namespace Runtime\Swoole\Tests\Unit;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Runtime\Swoole\SymfonyHttpBridge;
7+
use Swoole\Http\Request;
8+
use Swoole\Http\Response;
9+
use Symfony\Component\HttpFoundation\BinaryFileResponse;
10+
use Symfony\Component\HttpFoundation\File\UploadedFile;
11+
use Symfony\Component\HttpFoundation\HeaderBag;
12+
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
13+
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
14+
use Symfony\Component\HttpFoundation\StreamedResponse;
15+
16+
/**
17+
* @author Piotr Kugla <[email protected]>
18+
*/
19+
class SymfonyHttpBridgeTest extends TestCase
20+
{
21+
public function testThatSwooleRequestIsConverted(): void
22+
{
23+
$request = $this->createMock(Request::class);
24+
$request->server = ['request_method' => 'post'];
25+
$request->header = ['content-type' => 'application/json'];
26+
$request->cookie = ['foo' => 'cookie'];
27+
$request->get = ['foo' => 'get'];
28+
$request->post = ['foo' => 'post'];
29+
$request->files = [
30+
'foo' => [
31+
'name' => 'file',
32+
'type' => 'image/png',
33+
'tmp_name' => '/tmp/file',
34+
'error' => UPLOAD_ERR_CANT_WRITE,
35+
'size' => 0,
36+
],
37+
];
38+
$request->expects(self::once())->method('rawContent')->willReturn('{"foo": "body"}');
39+
40+
$sfRequest = SymfonyHttpBridge::convertSwooleRequest($request);
41+
42+
$this->assertSame(['REQUEST_METHOD' => 'post'], $sfRequest->server->all());
43+
$this->assertSame(['content-type' => ['application/json']], $sfRequest->headers->all());
44+
$this->assertSame(['foo' => 'cookie'], $sfRequest->cookies->all());
45+
$this->assertSame(['foo' => 'get'], $sfRequest->query->all());
46+
$this->assertSame(['foo' => 'post'], $sfRequest->request->all());
47+
$this->assertEquals('{"foo": "body"}', $sfRequest->getContent());
48+
49+
$this->assertCount(1, $sfRequest->files);
50+
51+
/** @var UploadedFile $file */
52+
$file = $sfRequest->files->get('foo');
53+
$this->assertNotNull($file);
54+
$this->assertEquals('file', $file->getClientOriginalName());
55+
$this->assertEquals('image/png', $file->getClientMimeType());
56+
$this->assertEquals('/tmp/file', $file->getPathname());
57+
$this->assertEquals(UPLOAD_ERR_CANT_WRITE, $file->getError());
58+
}
59+
60+
public function testThatSymfonyResponseIsReflected(): void
61+
{
62+
$sfResponse = $this->createMock(SymfonyResponse::class);
63+
$sfResponse->headers = new HeaderBag(['X-Test' => 'Swoole-Runtime']);
64+
$sfResponse->expects(self::once())->method('getStatusCode')->willReturn(201);
65+
$sfResponse->expects(self::once())->method('getContent')->willReturn('Test');
66+
67+
$response = $this->createMock(Response::class);
68+
$response->expects(self::once())->method('header')->with('x-test', 'Swoole-Runtime');
69+
$response->expects(self::once())->method('status')->with(201);
70+
$response->expects(self::once())->method('end')->with('Test');
71+
72+
SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response);
73+
}
74+
75+
public function testThatSymfonyStreamedResponseIsReflected(): void
76+
{
77+
$sfResponse = new StreamedResponse(function () {
78+
echo "Foo\n";
79+
ob_flush();
80+
81+
echo "Bar\n";
82+
ob_flush();
83+
});
84+
85+
$response = $this->createMock(Response::class);
86+
$response->expects(self::exactly(3))->method('write')->withConsecutive(["Foo\n"], ["Bar\n"], ['']);
87+
$response->expects(self::once())->method('end');
88+
89+
SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response);
90+
}
91+
92+
public function testThatSymfonyBinaryFileResponseIsReflected(): void
93+
{
94+
$file = tempnam(sys_get_temp_dir(), uniqid());
95+
file_put_contents($file, 'Foo');
96+
97+
$sfResponse = new BinaryFileResponse($file);
98+
99+
$response = $this->createMock(Response::class);
100+
$response->expects(self::once())->method('sendfile')->with($file, null, null);
101+
102+
SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response);
103+
}
104+
105+
public function testThatSymfonyBinaryFileResponseWithRangeIsReflected(): void
106+
{
107+
$file = tempnam(sys_get_temp_dir(), uniqid());
108+
file_put_contents($file, 'FooBar');
109+
110+
$request = new SymfonyRequest();
111+
$request->headers->set('Range', 'bytes=2-4');
112+
113+
$sfResponse = new BinaryFileResponse($file);
114+
$sfResponse->prepare($request);
115+
116+
$response = $this->createMock(Response::class);
117+
$response->expects(self::once())->method('write')->with('oBa');
118+
$response->expects(self::once())->method('end');
119+
120+
SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response);
121+
}
122+
}

tests/Unit/SymfonyRunnerTest.php

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use Swoole\Http\Request;
1111
use Swoole\Http\Response;
1212
use Swoole\Http\Server;
13-
use Symfony\Component\HttpFoundation\HeaderBag;
13+
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
1414
use Symfony\Component\HttpKernel\HttpKernelInterface;
1515

1616
class SymfonyRunnerTest extends TestCase
@@ -32,22 +32,15 @@ public function testRun(): void
3232

3333
public function testHandle(): void
3434
{
35-
$sfResponse = $this->createMock(\Symfony\Component\HttpFoundation\Response::class);
36-
$sfResponse->headers = new HeaderBag(['X-Test' => 'Swoole-Runtime']);
37-
$sfResponse->expects(self::once())->method('getStatusCode')->willReturn(201);
38-
$sfResponse->expects(self::once())->method('getContent')->willReturn('Test');
35+
$sfResponse = new SymfonyResponse('foo');
3936

4037
$application = $this->createMock(HttpKernelInterface::class);
4138
$application->expects(self::once())->method('handle')->willReturn($sfResponse);
4239

43-
$request = $this->createMock(Request::class);
44-
$request->header = [];
45-
4640
$response = $this->createMock(Response::class);
47-
$response->expects(self::once())->method('header')->with('x-test', 'Swoole-Runtime');
48-
$response->expects(self::once())->method('status')->with(201);
49-
$response->expects(self::once())->method('end')->with('Test');
41+
$response->expects(self::once())->method('end')->with('foo');
5042

43+
$request = $this->createMock(Request::class);
5144
$factory = $this->createMock(ServerFactory::class);
5245

5346
$runner = new SymfonyRunner($factory, $application);

0 commit comments

Comments
 (0)