diff --git a/src/Response/SwooleEmitter.php b/src/Response/SwooleEmitter.php new file mode 100644 index 00000000..431501e3 --- /dev/null +++ b/src/Response/SwooleEmitter.php @@ -0,0 +1,84 @@ +swooleResponse = $swooleResponse; + } + + /** + * Emits a response for the Swoole environment. + * + * @param ResponseInterface $response + */ + public function emit(ResponseInterface $response) + { + $this->emitStatusCode($response); + $this->emitHeaders($response); + $this->emitBody($response); + } + + /** + * Emit the message body. + * + * @param ResponseInterface $response + */ + private function emitBody(ResponseInterface $response) + { + $body = $response->getBody(); + $body->rewind(); + if ($body->getSize() > static::CHUNK_SIZE) { + while (! $body->eof()) { + $this->swooleResponse->write($body->read(static::CHUNK_SIZE)); + } + $this->swooleResponse->end(); + } else { + $this->swooleResponse->end($body->getContents()); + } + } + + /** + * Emit the headers + * + * @param ResponseInterface $response + */ + private function emitHeaders(ResponseInterface $response) + { + foreach ($response->getHeaders() as $name => $values) { + $name = $this->filterHeader($name); + $this->swooleResponse->header($name, implode(', ', $values)); + } + } + + /** + * Emit the status code + * + * @param ResponseInterface $response + */ + private function emitStatusCode(ResponseInterface $response) + { + $this->swooleResponse->status($response->getStatusCode()); + } +} diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index 57cdc167..ecabc070 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -9,7 +9,9 @@ use InvalidArgumentException; use Psr\Http\Message\UploadedFileInterface; +use RuntimeException; use stdClass; +use swoole_http_request; use UnexpectedValueException; use function array_change_key_case; @@ -99,6 +101,48 @@ public static function fromGlobals( ); } + /** + * Create a request from a Swoole request + * + * @param swoole_http_request $request + * @return ServerRequest + * @throws RuntimeException if Swoole is not installed + */ + + public static function fromSwoole(swoole_http_request $request) + { + if (! extension_loaded('swoole')) { + throw new Exception\RuntimeException('Swoole extension is not installed!'); + } + $get = isset($request->get) ? $request->get : []; + $post = isset($request->post) ? $request->post : []; + $cookie = isset($request->cookie) ? $request->cookie : []; + $files = isset($request->files) ? $request->files : []; + + $server = []; + foreach ($request->server as $key => $value) { + $server[strtoupper($key)] = $value; + } + + $headers = []; + foreach ($request->header as $name => $value) { + $headers[str_replace('-', '_', $name)] = $value; + } + + return new ServerRequest( + $server, + static::normalizeFiles($files), + static::marshalUriFromServer($server, $headers), + $server['REQUEST_METHOD'], + $request->rawContent(), + $headers, + $cookie, + $get, + $post, + static::marshalProtocolVersion($server) + ); + } + /** * Access a value in an array, returning a default value if not found * diff --git a/test/Response/SwooleEmitterTest.php b/test/Response/SwooleEmitterTest.php new file mode 100644 index 00000000..7218540f --- /dev/null +++ b/test/Response/SwooleEmitterTest.php @@ -0,0 +1,87 @@ +swooleResponse = $this->prophesize(swoole_http_response::class); + $this->emitter = new SwooleEmitter($this->swooleResponse->reveal()); + } + + public function testEmit() + { + $response = (new Response()) + ->withStatus(200) + ->withAddedHeader('Content-Type', 'text/plain'); + $response->getBody()->write('Content!'); + + $this->emitter->emit($response); + + $this->swooleResponse->status(200)->shouldHaveBeenCalled(); + $this->swooleResponse->header('Content-Type', 'text/plain') + ->shouldHaveBeenCalled(); + $this->swooleResponse->end('Content!')->shouldHaveBeenCalled(); + } + + public function testMultipleHeaders() + { + $response = (new Response()) + ->withStatus(200) + ->withHeader('Content-Type', 'text/plain') + ->withHeader('Content-Length', '256'); + + $this->emitter->emit($response); + + $this->swooleResponse->status(200)->shouldHaveBeenCalled(); + $this->swooleResponse->header('Content-Type', 'text/plain') + ->shouldHaveBeenCalled(); + $this->swooleResponse->header('Content-Length', '256') + ->shouldHaveBeenCalled(); + } + + public function testMultipleSetCookieHeaders() + { + $response = (new Response()) + ->withStatus(200) + ->withAddedHeader('Set-Cookie', 'foo=bar') + ->withAddedHeader('Set-Cookie', 'bar=baz'); + + $this->emitter->emit($response); + + $this->swooleResponse->status(200)->shouldHaveBeenCalled(); + $this->swooleResponse->header('Set-Cookie', 'foo=bar, bar=baz') + ->shouldHaveBeenCalled(); + } + + public function testEmitWithBigContentBody() + { + $content = base64_encode(random_bytes(SwooleEmitter::CHUNK_SIZE)); // CHUNK_SIZE * 1.33333 + $response = (new Response()) + ->withStatus(200) + ->withAddedHeader('Content-Type', 'text/plain'); + $response->getBody()->write($content); + + $this->emitter->emit($response); + + $this->swooleResponse->status(200)->shouldHaveBeenCalled(); + $this->swooleResponse->header('Content-Type', 'text/plain') + ->shouldHaveBeenCalled(); + $this->swooleResponse->write(substr($content, 0, SwooleEmitter::CHUNK_SIZE)) + ->shouldHaveBeenCalled(); + $this->swooleResponse->write(substr($content, SwooleEmitter::CHUNK_SIZE)) + ->shouldHaveBeenCalled(); + $this->swooleResponse->end()->shouldHaveBeenCalled(); + } +} diff --git a/test/ServerRequestFactoryTest.php b/test/ServerRequestFactoryTest.php index 63f202e1..36b9f06b 100644 --- a/test/ServerRequestFactoryTest.php +++ b/test/ServerRequestFactoryTest.php @@ -10,6 +10,7 @@ use PHPUnit\Framework\TestCase; use ReflectionMethod; use ReflectionProperty; +use swoole_http_request; use UnexpectedValueException; use Zend\Diactoros\ServerRequest; use Zend\Diactoros\ServerRequestFactory; @@ -580,4 +581,34 @@ public function marshalProtocolVersionProvider() 'HTTP/2' => ['HTTP/2', '2'], ]; } + + public function testFromSwoole() + { + if (! extension_loaded('swoole')) { + $this->markTestSkipped('The Swoole extesion is not available'); + } + + $swooleRequest = $this->createMock(swoole_http_request::class); + $swooleRequest->header['host'] = 'localhost:9501'; + $swooleRequest->server['request_method'] = 'GET'; + $swooleRequest->server['request_uri'] = '/'; + $swooleRequest->server['path_info'] = '/'; + $swooleRequest->server['request_time'] = time(); + $swooleRequest->server['server_protocol'] = 'HTTP/1.1'; + $swooleRequest->server['server_port'] = 9501; + $swooleRequest->server['remote_port'] = 45314; + $swooleRequest->method('rawContent')->willReturn('php://input'); + + $request = ServerRequestFactory::fromSwoole($swooleRequest); + $this->assertInstanceOf(ServerRequest::class, $request); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/', $request->getRequestTarget()); + $this->assertEquals('/', $request->getUri()->getPath()); + $this->assertEquals('1.1', $request->getProtocolVersion()); + $this->assertEquals('', $request->getBody()); + $this->assertEquals([], $request->getCookieParams()); + $this->assertEquals([], $request->getQueryParams()); + $this->assertEquals([], $request->getUploadedFiles()); + $this->assertEquals(['host' => ['localhost:9501']], $request->getHeaders()); + } }