diff --git a/README.md b/README.md index 8c6a923..d5e9b57 100644 --- a/README.md +++ b/README.md @@ -24,17 +24,13 @@ require __DIR__ . '/../vendor/autoload.php'; $app = new FrameworkX\App(); $app->get('/', function () { - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( "Hello wörld!\n" ); }); $app->get('/users/{name}', function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( "Hello " . $request->getAttribute('name') . "!\n" ); }); diff --git a/docs/api/app.md b/docs/api/app.md index 81a926b..a7fd6ad 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -23,12 +23,12 @@ multiple routes using inline closures like this: ```php $app->get('/user', function () { - return new React\Http\Message\Response(200, [], "hello everybody!"); + return React\Http\Message\Response::plaintext("Hello everybody!\n"); }); $app->get('/user/{id}', function (Psr\Http\Message\ServerRequestInterface $request) { $id = $request->getAttribute('id'); - return new React\Http\Message\Response(200, [], "hello $id"); + return React\Http\Message\Response::plaintext("Hello $id!\n"); }); ``` @@ -76,17 +76,17 @@ The `App` also offers a convenient helper method to redirect a matching route to a new URL like this: ```php -$app->redirect('/promo/reactphp', 'http://reactphp.org/'); +$app->redirect('/promo/reactphp', 'https://reactphp.org/'); ``` Browsers and search engine crawlers will automatically follow the redirect with the `302 Found` status code by default. You can optionally pass a custom redirect status code in the `3xx` range to use. If this is a permanent redirect, you may -want to use the `301 Permanent Redirect` status code to instruct search engine +want to use the `301 Moved Permanently` status code to instruct search engine crawlers to update their index like this: ```php -$app->redirect('/blog.html', '/blog', 301); +$app->redirect('/blog.html', '/blog', React\Http\Message\Response::STATUS_MOVED_PERMANENTLY); ``` See [response status codes](response.md#status-codes) for more details. @@ -98,9 +98,7 @@ examples more concise: ``` $app->get('/', function () { - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( "Hello wörld!\n" ); }); @@ -144,9 +142,7 @@ class HelloController { public function __invoke() { - return new Response( - 200, - [], + return Response::plaintext( "Hello wörld!\n" ); } diff --git a/docs/api/middleware.md b/docs/api/middleware.md index 2eabb29..b199545 100644 --- a/docs/api/middleware.md +++ b/docs/api/middleware.md @@ -20,7 +20,7 @@ $app->get( '/user', function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { // optionally return response without passing to next handler - // return new React\Http\Message\Response(403, [], "Forbidden!\n"); + // return React\Http\Message\Response::plaintext("Done.\n"); // optionally modify request before passing to next handler // $request = $request->withAttribute('admin', false); @@ -36,7 +36,7 @@ $app->get( }, function (Psr\Http\Message\ServerRequestInterface $request) { $role = $request->getAttribute('admin') ? 'admin' : 'user'; - return new React\Http\Message\Response(200, [], "Hello $role!\n"); + return React\Http\Message\Response::plaintext("Hello $role!\n"); } ); ``` @@ -65,7 +65,7 @@ class DemoMiddleware public function __invoke(ServerRequestInterface $request, callable $next) { // optionally return response without passing to next handler - // return new React\Http\Message\Response(403, [], "Forbidden!\n"); + // return React\Http\Message\Response::plaintext("Done.\n"); // optionally modify request before passing to next handler // $request = $request->withAttribute('admin', false); @@ -156,7 +156,7 @@ class UserController public function __invoke(ServerRequestInterface $request) { $role = $request->getAttribute('admin') ? 'admin' : 'user'; - return new Response(200, [], "Hello $role!\n"); + return Response::plaintext("Hello $role!\n"); } } ``` @@ -235,7 +235,7 @@ class UserController public function __invoke(ServerRequestInterface $request) { $name = 'Alice'; - return new Response(200, [], "Hello $name!\n"); + return Response::plaintext("Hello $name!\n"); } } ``` @@ -416,7 +416,7 @@ a response object synchronously: $name = yield $promise; assert(is_string($name)); - return new Response(200, [], "Hello $name!\n"); + return Response::plaintext("Hello $name!\n"); } /** @@ -455,7 +455,7 @@ a response object synchronously: { // async pseudo code to load some data from an external source return $this->fetchRandomUserName()->then(function (string $name) { - return new Response(200, [], "Hello $name!\n"); + return Response::plaintext("Hello $name!\n"); }); } diff --git a/docs/api/request.md b/docs/api/request.md index 033b688..f9dd7aa 100644 --- a/docs/api/request.md +++ b/docs/api/request.md @@ -33,7 +33,7 @@ You can access request attributes like this: $app->get('/user/{id}', function (Psr\Http\Message\ServerRequestInterface $request) { $id = $request->getAttribute('id'); - return new React\Http\Message\Response(200, [], "Hello $id"); + return React\Http\Message\Response::plaintext("Hello $id!\n"); }); ``` @@ -41,7 +41,7 @@ An HTTP request can be sent like this: ```bash $ curl http://localhost:8080/user/Alice -Hello Alice +Hello Alice! ``` These custom attributes are most commonly used when using URI placeholders @@ -63,7 +63,7 @@ $app->post('/user', function (Psr\Http\Message\ServerRequestInterface $request) $data = json_decode((string) $request->getBody()); $name = $data->name ?? 'anonymous'; - return new React\Http\Message\Response(200, [], "Hello $name"); + return React\Http\Message\Response::plaintext("Hello $name!\n"); }); ``` @@ -71,7 +71,7 @@ An HTTP request can be sent like this: ```bash $ curl http://localhost:8080/user --data '{"name":"Alice"}' -Hello Alice +Hello Alice! ``` Additionally, you may want to validate the `Content-Type: application/json` request header @@ -89,7 +89,7 @@ $app->post('/user', function (Psr\Http\Message\ServerRequestInterface $request) $data = $request->getParsedBody(); $name = $data['name'] ?? 'Anonymous'; - return new React\Http\Message\Response(200, [], "Hello $name"); + return React\Http\Message\Response::plaintext("Hello $name!\n"); }); ``` @@ -98,7 +98,7 @@ An HTTP request can be sent like this: ```bash $ curl http://localhost:8080/user -d name=Alice -Hello Alice +Hello Alice! ``` This method returns a possibly nested array of form fields, very similar to @@ -113,7 +113,7 @@ $app->post('/user', function (Psr\Http\Message\ServerRequestInterface $request) $files = $request->getUploadedFiles(); $name = isset($files['image']) ? $files['image']->getClientFilename() : 'x'; - return new React\Http\Message\Response(200, [], "Uploaded $name"); + return React\Http\Message\Response::plaintext("Uploaded $name\n"); }); ``` @@ -161,7 +161,7 @@ You can access all HTTP request headers like this: $app->get('/user', function (Psr\Http\Message\ServerRequestInterface $request) { $agent = $request->getHeaderLine('User-Agent'); - return new React\Http\Message\Response(200, [], "Hello $agent"); + return React\Http\Message\Response::plaintext("Hello $agent\n"); }); ``` @@ -184,7 +184,7 @@ $app->get('/user', function (Psr\Http\Message\ServerRequestInterface $request) { $params = $request->getServerParams(); $ip = $params['REMOTE_ADDR'] ?? 'unknown'; - return new React\Http\Message\Response(200, [], "Hello $ip"); + return React\Http\Message\Response::plaintext("Hello $ip\n"); }); ``` diff --git a/docs/api/response.md b/docs/api/response.md index 28119a4..df580e8 100644 --- a/docs/api/response.md +++ b/docs/api/response.md @@ -26,35 +26,10 @@ Here's everything you need to know to get started. You can send JSON data as an HTTP response body like this: ```php -$app->get('/user', function () { - $data = [ - [ - 'name' => 'Alice' - ], - [ - 'name' => 'Bob' - ] - ]; - - return new React\Http\Message\Response( - 200, - ['Content-Type' => 'application/json'], - json_encode($data) - ); -}); -``` - -An HTTP request can be sent like this: - -```bash -$ curl http://localhost:8080/user -[{"name":"Alice"},{"name":"Bob"}] -``` +get('/user', function () { $data = [ [ @@ -65,18 +40,18 @@ $app->get('/user', function () { ] ]; - return new React\Http\Message\Response( - 200, - ['Content-Type' => 'application/json'], - json_encode( - $data, - JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE - ) + return React\Http\Message\Response::json( + $data ); }); ``` -An HTTP request can be sent like this: +This example returns a simple JSON response from some static data. +In real-world applications, you may want to load this from a +[database](../integrations/database.md). +For common API usage, you may also want to receive a [JSON request](request.md#json). + +You can try out this example by sending an HTTP request like this: ```bash $ curl http://localhost:8080/user @@ -85,72 +60,128 @@ $ curl http://localhost:8080/user "name": "Alice" }, { - "name":"Bob" + "name": "Bob" } ] ``` -This example returns a simple JSON response from some static data. -In real-world applications, you may want to load this from a -[database](../integrations/database.md). -For common API usage, you may also want to receive a [JSON request](request.md#json). +> By default, the response will use the `200 OK` status code and will +> automatically include an appropriate `Content-Type: application/json` response +> header, see also [status codes](#status-codes) and [response headers](#headers) +> below for more details. +> +> If you want more control over the response such as using custom JSON flags, +> you can also manually create a [`React\Http\Message\Response`](https://reactphp.org/http/#response) +> object like this: +> +> ```php +> +> // … +> +> $app->get('/user', function () { +> $data = []; +> +> return new React\Http\Message\Response( +> React\Http\Message\Response::STATUS_OK, +> [ +> 'Content-Type' => 'application/json' +> ], +> json_encode( +> $data, +> JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE +> ) . "\n" +> ); +> }); +> ``` ## HTML You can send HTML data as an HTTP response body like this: ```php +get('/user', function () { $html = <<Hello Alice + HTML; - return new React\Http\Message\Response( - 200, - ['Content-Type' => 'text/html; charset=utf-8'], + return React\Http\Message\Response::html( $html ); }); ``` -An HTTP request can be sent like this: +This example returns a simple HTML response from some static data. +In real-world applications, you may want to load this from a +[database](../integrations/database.md) and perhaps use +[templates](../integrations/templates.md) to render your HTML. + +You can try out this example by sending an HTTP request like this: ```bash $ curl http://localhost:8080/user
' . $info . '' . "\n" ); }); @@ -90,7 +76,7 @@ }); return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, [ 'Content-Type' => 'text/plain;charset=utf-8' ], @@ -103,9 +89,7 @@ $app->redirect('/source', '/source/'); $app->any('/method', function (ServerRequestInterface $request) { - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( $request->getMethod() . "\n" ); }); @@ -114,16 +98,15 @@ $etag = '"_"'; if ($request->getHeaderLine('If-None-Match') === $etag) { return new React\Http\Message\Response( - 304, + React\Http\Message\Response::STATUS_NOT_MODIFIED, [ 'ETag' => $etag - ], - '' + ] ); } return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, [ 'ETag' => $etag ], @@ -134,17 +117,16 @@ $etag = '"' . $request->getAttribute('etag') . '"'; if ($request->getHeaderLine('If-None-Match') === $etag) { return new React\Http\Message\Response( - 304, + React\Http\Message\Response::STATUS_NOT_MODIFIED, [ 'ETag' => $etag, 'Content-Length' => strlen($etag) - 1 - ], - '' + ] ); } return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, [ 'ETag' => $etag ], @@ -156,15 +138,8 @@ // Returns a JSON representation of all request headers passed to this endpoint. // Note that this assumes UTF-8 data in request headers and may break for other encodings, // see also JSON_INVALID_UTF8_SUBSTITUTE (PHP 7.2+) or JSON_THROW_ON_ERROR (PHP 7.3+) - return new React\Http\Message\Response( - 200, - [ - 'Content-Type' => 'application/json' - ], - json_encode( - (object) array_map(function (array $headers) { return implode(', ', $headers); }, $request->getHeaders()), - JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_SLASHES - ) . "\n" + return React\Http\Message\Response::json( + (object) array_map(function (array $headers) { return implode(', ', $headers); }, $request->getHeaders()) ); }); diff --git a/src/AccessLogHandler.php b/src/AccessLogHandler.php index d0a5c4c..5c0a3f5 100644 --- a/src/AccessLogHandler.php +++ b/src/AccessLogHandler.php @@ -4,6 +4,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use React\Http\Message\Response; use React\Promise\PromiseInterface; use React\Stream\ReadableStreamInterface; @@ -79,7 +80,7 @@ private function log(ServerRequestInterface $request, ResponseInterface $respons $status = $response->getStatusCode(); // HEAD requests and `204 No Content` and `304 Not Modified` always use an empty response body - if ($method === 'HEAD' || $status === 204 || $status === 304) { + if ($method === 'HEAD' || $status === Response::STATUS_NO_CONTENT || $status === Response::STATUS_NOT_MODIFIED) { $responseSize = 0; } diff --git a/src/App.php b/src/App.php index 0b27246..cf8ed4e 100644 --- a/src/App.php +++ b/src/App.php @@ -6,6 +6,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\HttpServer; +use React\Http\Message\Response; use React\Promise\Deferred; use React\Promise\PromiseInterface; use React\Socket\SocketServer; @@ -162,7 +163,7 @@ public function map(array $methods, string $route, $handler, ...$handlers): void * @param string $target * @param int $code */ - public function redirect(string $route, string $target, int $code = 302): void + public function redirect(string $route, string $target, int $code = Response::STATUS_FOUND): void { $this->any($route, new RedirectHandler($target, $code)); } diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php index 100c747..40691a1 100644 --- a/src/ErrorHandler.php +++ b/src/ErrorHandler.php @@ -4,6 +4,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use React\Http\Message\Response; use React\Promise\PromiseInterface; /** @@ -107,7 +108,7 @@ private function coroutine(\Generator $generator): \Generator public function requestNotFound(): ResponseInterface { return $this->htmlResponse( - 404, + Response::STATUS_NOT_FOUND, 'Page Not Found', 'Please check the URL in the address bar and try again.' ); @@ -118,7 +119,7 @@ public function requestMethodNotAllowed(array $allowedMethods): ResponseInterfac $methods = \implode('/', \array_map(function (string $method) { return '
' . $method . '
'; }, $allowedMethods));
return $this->htmlResponse(
- 405,
+ Response::STATUS_METHOD_NOT_ALLOWED,
'Method Not Allowed',
'Please check the URL in the address bar and try again with ' . $methods . ' request.'
)->withHeader('Allow', \implode(', ', $allowedMethods));
@@ -127,7 +128,7 @@ public function requestMethodNotAllowed(array $allowedMethods): ResponseInterfac
public function requestProxyUnsupported(): ResponseInterface
{
return $this->htmlResponse(
- 400,
+ Response::STATUS_BAD_REQUEST,
'Proxy Requests Not Allowed',
'Please check your settings and retry.'
);
@@ -139,7 +140,7 @@ public function errorInvalidException(\Throwable $e): ResponseInterface
$message = '' . $this->html->escape($e->getMessage()) . '
';
return $this->htmlResponse(
- 500,
+ Response::STATUS_INTERNAL_SERVER_ERROR,
'Internal Server Error',
'The requested page failed to load, please try again later.',
'Expected request handler to return ' . ResponseInterface::class . '
but got uncaught ' . \get_class($e) . '
with message ' . $message . $where . '.'
@@ -149,7 +150,7 @@ public function errorInvalidException(\Throwable $e): ResponseInterface
public function errorInvalidResponse($value): ResponseInterface
{
return $this->htmlResponse(
- 500,
+ Response::STATUS_INTERNAL_SERVER_ERROR,
'Internal Server Error',
'The requested page failed to load, please try again later.',
'Expected request handler to return ' . ResponseInterface::class . '
but got ' . $this->describeType($value) . '
.'
@@ -161,7 +162,7 @@ public function errorInvalidCoroutine($value, string $file, int $line): Response
$where = ' near or before '. $this->where($file, $line) . '.';
return $this->htmlResponse(
- 500,
+ Response::STATUS_INTERNAL_SERVER_ERROR,
'Internal Server Error',
'The requested page failed to load, please try again later.',
'Expected request handler to yield ' . PromiseInterface::class . '
but got ' . $this->describeType($value) . '
' . $where
diff --git a/src/FilesystemHandler.php b/src/FilesystemHandler.php
index acc2642..5deaf47 100644
--- a/src/FilesystemHandler.php
+++ b/src/FilesystemHandler.php
@@ -88,11 +88,7 @@ public function __invoke(ServerRequestInterface $request)
}
$response .= '' . "\n";
- return new Response(
- 200,
- [
- 'Content-Type' => 'text/html; charset=utf-8'
- ],
+ return Response::html(
$response
);
} elseif ($valid && \is_file($path)) {
@@ -112,12 +108,12 @@ public function __invoke(ServerRequestInterface $request)
$headers['Last-Modified'] = \gmdate('D, d M Y H:i:s', $stat['mtime']) . ' GMT';
if ($request->getHeaderLine('If-Modified-Since') === $headers['Last-Modified']) {
- return new Response(304);
+ return new Response(Response::STATUS_NOT_MODIFIED);
}
}
return new Response(
- 200,
+ Response::STATUS_OK,
$headers,
\file_get_contents($path)
);
diff --git a/src/RedirectHandler.php b/src/RedirectHandler.php
index 80a9875..737d9c2 100644
--- a/src/RedirectHandler.php
+++ b/src/RedirectHandler.php
@@ -17,9 +17,9 @@ class RedirectHandler
/** @var HtmlHandler */
private $html;
- public function __construct(string $target, int $redirectStatusCode = 302)
+ public function __construct(string $target, int $redirectStatusCode = Response::STATUS_FOUND)
{
- if ($redirectStatusCode < 300 || $redirectStatusCode === 304 || $redirectStatusCode >= 400) {
+ if ($redirectStatusCode < 300 || $redirectStatusCode === Response::STATUS_NOT_MODIFIED || $redirectStatusCode >= 400) {
throw new \InvalidArgumentException('Invalid redirect status code given');
}
diff --git a/src/SapiHandler.php b/src/SapiHandler.php
index e34f86f..ecf776c 100644
--- a/src/SapiHandler.php
+++ b/src/SapiHandler.php
@@ -4,6 +4,7 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
+use React\Http\Message\Response;
use React\Http\Message\ServerRequest;
use React\Stream\ReadableStreamInterface;
@@ -87,10 +88,10 @@ public function sendResponse(ResponseInterface $response): void
header($_SERVER['SERVER_PROTOCOL'] . ' ' . $status . ' ' . $response->getReasonPhrase());
- if ($status === 204) {
- // 204 MUST NOT include "Content-Length" response header
+ if ($status === Response::STATUS_NO_CONTENT) {
+ // `204 No Content` MUST NOT include "Content-Length" response header
$response = $response->withoutHeader('Content-Length');
- } elseif (!$response->hasHeader('Content-Length') && $body->getSize() !== null && ($status !== 304 || $body->getSize() !== 0)) {
+ } elseif (!$response->hasHeader('Content-Length') && $body->getSize() !== null && ($status !== Response::STATUS_NOT_MODIFIED || $body->getSize() !== 0)) {
// automatically assign "Content-Length" response header if known and not already present
$response = $response->withHeader('Content-Length', (string) $body->getSize());
}
@@ -111,7 +112,7 @@ public function sendResponse(ResponseInterface $response): void
}
ini_set('default_charset', $old);
- if (($_SERVER['REQUEST_METHOD'] ?? '') === 'HEAD' || $status === 204 || $status === 304) {
+ if (($_SERVER['REQUEST_METHOD'] ?? '') === 'HEAD' || $status === Response::STATUS_NO_CONTENT || $status === Response::STATUS_NOT_MODIFIED) {
$body->close();
return;
}
diff --git a/tests/acceptance.sh b/tests/acceptance.sh
index d18c531..5ecb1fd 100755
--- a/tests/acceptance.sh
+++ b/tests/acceptance.sh
@@ -19,7 +19,7 @@ skipif() {
echo "$out" | grep "$@" >/dev/null && echo -n S && return 1 || return 0
}
-out=$(curl -v $base/ 2>&1); match "HTTP/.* 200" && notmatch -i "Content-Type:"
+out=$(curl -v $base/ 2>&1); match "HTTP/.* 200" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
out=$(curl -v $base/invalid 2>&1); match "HTTP/.* 404" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
out=$(curl -v $base// 2>&1); match "HTTP/.* 404"
out=$(curl -v $base/ 2>&1 -X POST); match "HTTP/.* 405"