diff --git a/README.md b/README.md index 09a4ecd2..0ea7c11c 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ multiple concurrent HTTP requests without blocking. * [withProtocolVersion()](#withprotocolversion) * [withResponseBuffer()](#withresponsebuffer) * [React\Http\Message](#reacthttpmessage) + * [ResponseFactory](#responsefactory) * [Response](#response) * [ServerRequest](#serverrequest) * [ResponseException](#responseexception) @@ -103,13 +104,7 @@ This is an HTTP server which responds with `Hello World!` to every request. $loop = React\EventLoop\Factory::create(); $server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "Hello World!\n" - ); + return React\Http\Message\ResponseFactory::plain("Hello World!\n"); }); $socket = new React\Socket\Server(8080, $loop); @@ -719,13 +714,7 @@ object and expects a [response](#server-response) object in return: ```php $server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "Hello World!\n" - ); + return React\Http\Message\ResponseFactory::plain("Hello World!\n"); }); ``` @@ -2374,6 +2363,19 @@ given setting applied. ### React\Http\Message +#### ResponseFactory + +The `React\Http\Message\ResponseFactory` provides a few methods for well known content types. Except `json` all methods +accept both string or [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) as body. +`json` however, will also do the encoding to JSON for you. + +```php +$htmlResponse = React\Http\Message\ResponseFactory::html('Hello world!'); +$jsonResponse = React\Http\Message\ResponseFactory::json(array('message' => array('body' => 'Hello World!'))); +$plainResponse = React\Http\Message\ResponseFactory::plain('Hello world!'); +$xmlResponse = React\Http\Message\ResponseFactory::xml('Hello world!'); +``` + #### Response The `React\Http\Message\Response` class can be used to diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index f6903cff..c0c7d1e3 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Message\Response; +use React\Http\Message\ResponseFactory; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; @@ -10,13 +10,7 @@ $loop = Factory::create(); $server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "Hello world\n" - ); + return ResponseFactory::plain("Hello world\n"); }); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 2b8e897c..35b29962 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Message\Response; +use React\Http\Message\ResponseFactory; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; @@ -11,13 +11,7 @@ $counter = 0; $server = new Server($loop, function (ServerRequestInterface $request) use (&$counter) { - return new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "Welcome number " . ++$counter . "!\n" - ); + return ResponseFactory::plain("Welcome number " . ++$counter . "!\n"); }); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 18f7504e..bab3b566 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Message\Response; +use React\Http\Message\ResponseFactory; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; @@ -12,13 +12,7 @@ $server = new Server($loop, function (ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; - return new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - $body - ); + return ResponseFactory::plain($body); }); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 2786f380..178ec3e7 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Message\Response; +use React\Http\Message\ResponseFactory; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; @@ -19,13 +19,7 @@ $body = 'The value of "foo" is: ' . htmlspecialchars($queryParams['foo']); } - return new Response( - 200, - array( - 'Content-Type' => 'text/html' - ), - $body - ); + return ResponseFactory::html($body); }); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index 6faf6be7..031dc6f4 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Message\Response; +use React\Http\Message\ResponseFactory; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; @@ -15,23 +15,10 @@ if (isset($request->getCookieParams()[$key])) { $body = "Your cookie value is: " . $request->getCookieParams()[$key]; - return new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - $body - ); + return ResponseFactory::plain($body); } - return new Response( - 200, - array( - 'Content-Type' => 'text/plain', - 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') - ), - "Your cookie has been set." - ); + return ResponseFactory::plain('Your cookie has been set.')->withAddedHeader('Set-Cookie', urlencode($key) . '=' . urlencode('test;more')); }); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index 3da6963b..40600853 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Message\Response; +use React\Http\Message\ResponseFactory; use React\Http\Server; use React\Promise\Promise; @@ -13,13 +13,7 @@ $server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($loop) { $loop->addTimer(1.5, function() use ($resolve) { - $response = new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "Hello world" - ); + $response = ResponseFactory::plain('Hello world'); $resolve($response); }); }); diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index c8e99ee4..0c8e586a 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Message\Response; +use React\Http\Message\ResponseFactory; use React\Http\Server; use React\Promise\Promise; @@ -19,13 +19,7 @@ throw new Exception('Second call'); } - $response = new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "Hello World!\n" - ); + $response = ResponseFactory::plain("Hello World!\n"); $resolve($response); }); diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index 518c2cb4..73a338eb 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -3,6 +3,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Message\Response; +use React\Http\Message\ResponseFactory; use React\Http\Server; use React\Stream\ThroughStream; @@ -32,13 +33,7 @@ $loop->cancelTimer($timer); }); - return new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - $stream - ); + return ResponseFactory::plain($stream); }); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 8602a889..1637f83a 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -8,7 +8,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Message\Response; +use React\Http\Message\ResponseFactory; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; @@ -17,43 +17,25 @@ $server = new Server($loop, function (ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { - return new Response( - 415, // Unsupported Media Type - array( - 'Content-Type' => 'application/json' - ), - json_encode(array('error' => 'Only supports application/json')) . "\n" - ); + return ResponseFactory::json( + array('error' => 'Only supports application/json') + )->withStatus(415); // Unsupported Media Type } $input = json_decode($request->getBody()->getContents()); if (json_last_error() !== JSON_ERROR_NONE) { - return new Response( - 400, // Bad Request - array( - 'Content-Type' => 'application/json' - ), - json_encode(array('error' => 'Invalid JSON data given')) . "\n" - ); + return ResponseFactory::json( + array('error' => 'Invalid JSON data given') + )->withStatus(400); // Bad Request } if (!isset($input->name) || !is_string($input->name)) { - return new Response( - 422, // Unprocessable Entity - array( - 'Content-Type' => 'application/json' - ), - json_encode(array('error' => 'JSON data does not contain a string "name" property')) . "\n" - ); + return ResponseFactory::json( + array('error' => 'JSON data does not contain a string "name" property') + )->withStatus(422); // Unprocessable Entity } - return new Response( - 200, - array( - 'Content-Type' => 'application/json' - ), - json_encode(array('message' => 'Hello ' . $input->name)) . "\n" - ); + return ResponseFactory::json(array('message' => 'Hello ' . $input->name)); }); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index dfe3e941..2625a83a 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Message\Response; +use React\Http\Message\ResponseFactory; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; @@ -10,13 +10,7 @@ $loop = Factory::create(); $server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "Hello world!\n" - ); + return ResponseFactory::plain("Hello world\n"); }); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 45eb0dea..39280866 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -1,6 +1,7 @@ on('end', function () use ($resolve, &$bytes){ - $resolve(new React\Http\Message\Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "Received $bytes bytes\n" - )); + $resolve(ResponseFactory::plain("Received $bytes bytes\n")); }); // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { - $resolve(new React\Http\Message\Response( - 400, - array( - 'Content-Type' => 'text/plain' - ), - "Encountered error after $bytes bytes: {$exception->getMessage()}\n" - )); + $resolve(ResponseFactory::plain("Encountered error after $bytes bytes: {$exception->getMessage()}\n")->withStatus(400)); }); }); } diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index b959b7bf..a70dc86c 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -5,7 +5,7 @@ use Psr\Http\Message\RequestInterface; use React\EventLoop\Factory; -use React\Http\Message\Response; +use React\Http\Message\ResponseFactory; use React\Http\Server; use RingCentral\Psr7; @@ -19,13 +19,7 @@ // `StreamingRequestMiddleware` to forward the incoming request as it comes in. $server = new Server($loop, function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { - return new Response( - 400, - array( - 'Content-Type' => 'text/plain' - ), - 'This is a plain HTTP proxy' - ); + return ResponseFactory::plain('This is a plain HTTP proxy')->withStatus(400); } // prepare outgoing client request by updating request-target and Host header @@ -39,13 +33,7 @@ // pseudo code only: simply dump the outgoing request as a string // left up as an exercise: use an HTTP client to send the outgoing request // and forward the incoming response to the original client request - return new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - Psr7\str($outgoing) - ); + return ResponseFactory::plain(Psr7\str($outgoing)); }); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index e786da76..46211805 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -6,6 +6,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Message\Response; +use React\Http\Message\ResponseFactory; use React\Http\Server; use React\Socket\Connector; use React\Socket\ConnectionInterface; @@ -21,14 +22,7 @@ // doesn't have to store any payload data in memory at all. $server = new Server($loop, function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { - return new Response( - 405, - array( - 'Content-Type' => 'text/plain', - 'Allow' => 'CONNECT' - ), - 'This is a HTTP CONNECT (secure HTTPS) proxy' - ); + return ResponseFactory::plain('This is a HTTP CONNECT (secure HTTPS) proxy')->withStatus(405); } // try to connect to given target host @@ -42,13 +36,7 @@ function (ConnectionInterface $remote) { ); }, function ($e) { - return new Response( - 502, - array( - 'Content-Type' => 'text/plain' - ), - 'Unable to connect: ' . $e->getMessage() - ); + return ResponseFactory::plain('Unable to connect: ' . $e->getMessage())->withStatus(502); } ); }); diff --git a/src/Message/ResponseFactory.php b/src/Message/ResponseFactory.php new file mode 100644 index 00000000..13772fb6 --- /dev/null +++ b/src/Message/ResponseFactory.php @@ -0,0 +1,80 @@ + 'text/html; charset=utf-8', + ), + $body + ); + } + + /** + * @param mixed $body + * @return ResponseInterface + */ + public static function json($body) + { + $json = @\json_encode($body); + + if (\json_last_error() !== JSON_ERROR_NONE || ($json === null && $body !== null)) { + if (\function_exists('json_last_error_msg')) { + throw new \InvalidArgumentException('Error encoding JSON: ' . \json_last_error_msg()); + } + + throw new \InvalidArgumentException('Error encoding JSON'); + } + + return new Response( + 200, + array( + 'Content-Type' => 'application/json; charset=utf-8', + ), + $json + ); + } + + /** + * @param string|ReadableStreamInterface|StreamInterface $body + * @return ResponseInterface + */ + public static function plain($body) + { + return new Response( + 200, + array( + 'Content-Type' => 'text/plain; charset=utf-8', + ), + $body + ); + } + + /** + * @param string|ReadableStreamInterface|StreamInterface $body + * @return ResponseInterface + */ + public static function xml($body) + { + return new Response( + 200, + array( + 'Content-Type' => 'application/xml; charset=utf-8', + ), + $body + ); + } +} diff --git a/tests/Message/ResponseFactoryTest.php b/tests/Message/ResponseFactoryTest.php new file mode 100644 index 00000000..2c145f3d --- /dev/null +++ b/tests/Message/ResponseFactoryTest.php @@ -0,0 +1,71 @@ +Hello world!'; + $response = ResponseFactory::html($body); + + self::assertSame($body, $response->getBody()->getContents()); + } + + public function provideJson() + { + return array( + 'string' => array( + 'Hello World!', + '"Hello World!"', + ), + 'array' => array( + array( + 'message' => array( + 'body' => 'Hello World!' + ) + ), + '{"message":{"body":"Hello World!"}}', + ), + ); + } + + /** + * @dataProvider provideJson + */ + public function testJson($json, $expectedBody) + { + $response = ResponseFactory::json($json); + + self::assertSame($expectedBody, $response->getBody()->getContents()); + } + + public function testInvalidJson() + { + $this->setExpectedException('InvalidArgumentException'); + + ResponseFactory::json("\xB1\x31"); + } + + public function testPlain() + { + $body = 'Hello world!'; + $response = ResponseFactory::plain($body); + + self::assertSame($body, $response->getBody()->getContents()); + } + + public function testXml() + { + $body = 'Hello world!'; + $response = ResponseFactory::xml($body); + + self::assertSame($body, $response->getBody()->getContents()); + } +}