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

Hello Alice

``` -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. +> By default, the response will use the `200 OK` status code and will +> automatically include an appropriate `Content-Type: text/html; charset=utf-8` +> response header, see also [status codes](#status-codes) and +> [response headers](#headers) below for more details. +> +> If you want more control over this behavior, you can also manually create a +> [`React\Http\Message\Response`](https://reactphp.org/http/#response) object +> like this: +> +> ```php +> +> // … +> +> $app->get('/user', function () { +> $html = "Hello Wörld!\n"; +> +> return new React\Http\Message\Response( +> React\Http\Message\Response::STATUS_OK, +> [ +> 'Content-Type' => 'text/html; charset=utf-8' +> ], +> $html +> ); +> }); +> ``` ## Status Codes -You can assign status codes like this: +The [`json()`](#json) and [`html()`](#html) methods used above automatically use +a `200 OK` status code by default. You can assign status codes like this: + +```php hl_lines="10" +get('/user/{id}', function (Psr\Http\Message\ServerRequestInterface $request) { $id = $request->getAttribute('id'); if ($id === 'admin') { - return new React\Http\Message\Response( - 403, - [], - 'Forbidden' - ); + return React\Http\Message\Response::html( + "Forbidden\n" + )->withStatus(React\Http\Message\Response::STATUS_FORBIDDEN); } - return new React\Http\Message\Response( - 200, - [], - "Hello $id" + return React\Http\Message\Response::html( + "Hello $id\n" ); }); ``` -An HTTP request can be sent like this: +You can try out this example by sending an HTTP request like this: -```bash hl_lines="2 6" +```bash $ curl -I http://localhost:8080/user/Alice HTTP/1.1 200 OK … @@ -160,51 +191,77 @@ HTTP/1.1 403 Forbidden … ``` -Each HTTP response message contains a status code that describes whether the -HTTP request has been successfully completed. -Here's a list of the most common HTTP status codes: - -* `200 OK` -* `301 Permanent Redirect` -* `302 Found` (previously `302 Temporary Redirect`) -* `304 Not Modified` (see [HTTP caching](#http-caching) below) -* `403 Forbidden` -* `404 Not Found` -* `500 Internal Server Error` -* … - -See [list of HTTP status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) for more details. +> Each HTTP response message contains a status code that describes whether the +> HTTP request has been successfully completed. Here's a list with some of the +> most common HTTP status codes: +> +> * `200 OK` +> * `301 Moved Permanently` +> * `302 Found` (previously `302 Temporary Redirect`) +> * `304 Not Modified` (see [HTTP caching](#http-caching) below) +> * `403 Forbidden` +> * `404 Not Found` +> * `500 Internal Server Error` +> * … +> +> See [list of HTTP status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) +> for more details. Each status code can be referenced by its matching status code +> constant name such as `React\Http\Message\Response::STATUS_OK` or `React\Http\Message\Response::STATUS_NOT_FOUND` +> or by its status code number. ## Headers -You can assign HTTP response headers like this: +The [`json()`](#json) and [`html()`](#html) methods used above automatically use +an appropriate `Content-Type` response header by default. You can assign HTTP +response headers like this: + +```php hl_lines="8" +get('/user', function () { - return new React\Http\Message\Response( - 200, - ['Content-Type' => 'text/plain; charset=utf-8'], - "Hello wörld" - ); + return React\Http\Message\Response::html( + "Hello wörld!\n" + )->withHeader('Cache-Control', 'public'); }); ``` -An HTTP request can be sent like this: +You can try out this example by sending an HTTP request like this: -```bash hl_lines="3" +```bash $ curl -I http://localhost:8080/user HTTP/1.1 200 OK -Content-Type: text/plain; charset=utf-8 -… -``` +Content-Type: text/html; charset=utf-8 +Cache-Control: public -Each HTTP response message can contain an arbitrary number of response headers. -You can pass these headers as an associative array to the response object. +Hello wörld! +``` -Additionally, the application will automatically include default headers required -by the HTTP protocol. -It's not recommended to mess with these default headers unless you're sure you -know what you're doing. +> Each HTTP response message can contain an arbitrary number of response +> headers. Additionally, the application will automatically include default +> headers required by the HTTP protocol. It's not recommended to mess with these +> default headers unless you're sure you know what you're doing. +> +> If you want more control over this behavior, you can also manually create a +> [`React\Http\Message\Response`](https://reactphp.org/http/#response) object like this: +> +> ```php +> +> // … +> +> $app->get('/user', function () { +> return new React\Http\Message\Response( +> React\Http\Message\Response::STATUS_OK, +> [ +> 'Content-Type' => 'text/html; charset=utf-8', +> 'Cache-Control' => 'public' +> ], +> "Hello wörld!\n" +> ); +> }); +> ``` ## HTTP caching @@ -228,6 +285,7 @@ response header to control the lifetime of a cached response like this: ```php get('/user', function () { @@ -235,18 +293,13 @@ $app->get('/user', function () {

Hello Alice

HTML; - return new Response( - 200, - [ - 'Content-Type' => 'text/html; charset=utf-8', - 'Cache-Control' => 'max-age=3600', - ], + return new React\Http\Message\Response::html( $html - ); + )->withHeader('Cache-Control', 'max-age=3600'); }); ``` -An HTTP request can be sent like this: +You can try out this example by sending an HTTP request like this: ```bash $ curl -I http://localhost:8080/user @@ -269,6 +322,7 @@ response and omit the response body like this: ```php get('/user', function (Psr\Http\Message\ServerRequestInterface $request) { @@ -279,8 +333,8 @@ HTML; $etag = '"' . sha1($html) . '"'; if ($request->getHeaderLine('If-None-Match') === $etag) { - return new Response( - 304, + return new React\Http\Message\Response( + React\Http\Message\Response::STATUS_NOT_MODIFIED, [ 'Cache-Control' => 'max-age=0, must-revalidate', 'ETag' => $etag @@ -288,8 +342,8 @@ HTML; ); } - return new Response( - 200, + return new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, [ 'Content-Type' => 'text/html; charset=utf-8', 'Cache-Control' => 'max-age=0, must-revalidate', @@ -300,7 +354,7 @@ HTML; }); ``` -An HTTP request can be sent like this: +You can try out this example by sending an HTTP request like this: ```bash $ curl http://localhost:8080/user @@ -327,6 +381,7 @@ response and omit the response body like this: ```php get('/user', function (Psr\Http\Message\ServerRequestInterface $request) { @@ -341,8 +396,8 @@ HTML; $modified = $date->setTimezone(new DateTimeZone('UTC'))->format(DATE_RFC7231); if ($request->getHeaderLine('If-Modified-Since') === $modified) { - return new Response( - 304, + return new React\Http\Message\Response( + React\Http\Message\Response::STATUS_NOT_MODIFIED, [ 'Cache-Control' => 'max-age=0, must-revalidate', 'Last-Modified' => $modified @@ -350,8 +405,8 @@ HTML; ); } - return new Response( - 200, + return new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, [ 'Content-Type' => 'text/html; charset=utf-8', 'Cache-Control' => 'max-age=0, must-revalidate', @@ -362,7 +417,7 @@ HTML; }); ``` -An HTTP request can be sent like this: +You can try out this example by sending an HTTP request like this: ```bash $ curl http://localhost:8080/user @@ -411,6 +466,7 @@ to catch any output and return it as a response body like this: ```php get('/dump', function () { @@ -419,15 +475,13 @@ $app->get('/dump', function () { var_dump(42); $body = ob_get_clean(); - return new React\Http\Message\Response( - 200, - ['Content-Type' => 'text/plain; charset=utf-8'], + return React\Http\Message\Response::plaintext( $body ); }); ``` -An HTTP request can be sent like this: +You can try out this example by sending an HTTP request like this: ```bash $ curl http://localhost:8080/dump @@ -460,12 +514,16 @@ HTTP request will automatically be rejected with a `500 Internal Server Error` HTTP error response: ```php +get('/user', function () { throw new BadMethodCallException(); }); ``` -An HTTP request can be sent like this: +You can try out this example by sending an HTTP request like this: ```bash hl_lines="2" $ curl -I http://localhost:8080/user diff --git a/docs/async/coroutines.md b/docs/async/coroutines.md index 33050ef..3833bf3 100644 --- a/docs/async/coroutines.md +++ b/docs/async/coroutines.md @@ -26,9 +26,7 @@ $app->get('/book', function () use ($db) { ); $data = "Found " . $result->resultRows[0]['count'] . " books\n"; - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( $data ); }); @@ -72,9 +70,7 @@ $app->get('/book', function () use ($db) { ); $data = "Found " . $result->resultRows[0]['count'] . " books\n"; - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( $data ); }); @@ -158,17 +154,13 @@ class BookLookupController assert($book === null || $book instanceof Book); if ($book === null) { - return new Response( - 404, - [], + return Response::plaintext( "Book not found\n" - ); + )->withStatus(Response::STATUS_NOT_FOUND); } $data = $book->title; - return new Response( - 200, - [], + return Response::plaintext( $data ); } diff --git a/docs/async/fibers.md b/docs/async/fibers.md index 04db94d..263c9b8 100644 --- a/docs/async/fibers.md +++ b/docs/async/fibers.md @@ -28,9 +28,7 @@ $app->get('/book', function () use ($db) { )); $data = "Found " . $result->resultRows[0]['count'] . " books\n"; - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( $data ); }); diff --git a/docs/async/promises.md b/docs/async/promises.md index 6a1ff16..01cb5ce 100644 --- a/docs/async/promises.md +++ b/docs/async/promises.md @@ -25,9 +25,7 @@ $app->get('/book', function () use ($db) { 'SELECT COUNT(*) AS count FROM book' )->then(function (React\MySQL\QueryResult $result) { $data = "Found " . $result->resultRows[0]['count'] . " books\n"; - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( $data ); }); @@ -70,9 +68,7 @@ $app->get('/book', function () use ($db) { 'SELECT COUNT(*) AS count FROM book' )->then(function (React\MySQL\QueryResult $result) { $data = "Found " . $result->resultRows[0]['count'] . " books\n"; - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( $data ); }); @@ -156,17 +152,13 @@ class BookLookupController $isbn = $request->getAttribute('isbn'); return $this->repository->findBook($isbn)->then(function (?Book $book) { if ($book === null) { - return new Response( - 404, - [], + return Response::plaintext( "Book not found\n" - ); + )->withStatus(Response::STATUS_NOT_FOUND; } $data = $book->title; - return new Response( - 200, - [], + return Response::plaintext( $data ); }); diff --git a/docs/best-practices/controllers.md b/docs/best-practices/controllers.md index f5d1b1c..eed6ce0 100644 --- a/docs/best-practices/controllers.md +++ b/docs/best-practices/controllers.md @@ -17,17 +17,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" ); }); @@ -85,9 +81,7 @@ class HelloController { public function __invoke() { - return new Response( - 200, - [], + return Response::plaintext( "Hello wörld!\n" ); } @@ -106,9 +100,7 @@ class UserController { public function __invoke(ServerRequestInterface $request) { - return new Response( - 200, - [], + return Response::plaintext( "Hello " . $request->getAttribute('name') . "!\n" ); } diff --git a/docs/best-practices/testing.md b/docs/best-practices/testing.md index 3b208dc..03f8061 100644 --- a/docs/best-practices/testing.md +++ b/docs/best-practices/testing.md @@ -33,9 +33,7 @@ class HelloController { public function __invoke() { - return new Response( - 200, - [], + return Response::plaintext( "Hello wörld!\n" ); } @@ -127,9 +125,7 @@ class UserController { public function __invoke(ServerRequestInterface $request) { - return new Response( - 200, - [], + return Response::plaintext( "Hello " . $request->getAttribute('name') . "!\n" ); } diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 1fe9d08..cf382c4 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -26,17 +26,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/integrations/database.md b/docs/integrations/database.md index 88b4e5e..68452f8 100644 --- a/docs/integrations/database.md +++ b/docs/integrations/database.md @@ -27,9 +27,7 @@ Let's take a look at the most basic async database integration possible with X: )); $data = "Found " . $result->resultRows[0]['count'] . " books\n"; - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( $data ); }); @@ -57,9 +55,7 @@ Let's take a look at the most basic async database integration possible with X: ); $data = "Found " . $result->resultRows[0]['count'] . " books\n"; - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( $data ); }); @@ -86,9 +82,7 @@ Let's take a look at the most basic async database integration possible with X: 'SELECT COUNT(*) AS count FROM book' )->then(function (React\MySQL\QueryResult $result) { $data = "Found " . $result->resultRows[0]['count'] . " books\n"; - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( $data ); }); @@ -234,17 +228,13 @@ from a [route placeholder](../api/app.md#routing) like this: assert($result instanceof React\MySQL\QueryResult); if (count($result->resultRows) === 0) { - return new React\Http\Message\Response( - 404, - [], + return React\Http\Message\Response::plaintext( "Book not found\n" - ); + )->withStatus(React\Http\Message\Response::STATUS_NOT_FOUND); } $data = $result->resultRows[0]['title']; - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( $data ); @@ -276,17 +266,13 @@ from a [route placeholder](../api/app.md#routing) like this: assert($result instanceof React\MySQL\QueryResult); if (count($result->resultRows) === 0) { - return new React\Http\Message\Response( - 404, - [], + return React\Http\Message\Response::plaintext( "Book not found\n" - ); + )->withStatus(React\Http\Message\Response::STATUS_NOT_FOUND); } $data = $result->resultRows[0]['title']; - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( $data ); @@ -318,17 +304,13 @@ from a [route placeholder](../api/app.md#routing) like this: if (count($result->resultRows) === 0) { - return new React\Http\Message\Response( - 404, - [], + return React\Http\Message\Response::plaintext( "Book not found\n" - ); + )->withStatus(React\Http\Message\Response::STATUS_NOT_FOUND); } $data = $result->resultRows[0]['title']; - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( $data ); }); @@ -560,17 +542,13 @@ interface with each other using a simple async API: if ($book === null) { - return new Response( - 404, - [], + return Response::plaintext( "Book not found\n" - ); + )->withStatus(Response::STATUS_NOT_FOUND); } $data = $book->title; - return new Response( - 200, - [], + return Response::plaintext( $data ); } @@ -641,17 +619,13 @@ interface with each other using a simple async API: assert($book === null || $book instanceof Book); if ($book === null) { - return new Response( - 404, - [], + return Response::plaintext( "Book not found\n" - ); + )->withStatus(Response::STATUS_NOT_FOUND); } $data = $book->title; - return new Response( - 200, - [], + return Response::plaintext( $data ); } @@ -721,17 +695,13 @@ interface with each other using a simple async API: return $this->repository->findBook($isbn)->then(function (?Book $book) { if ($book === null) { - return new Response( - 404, - [], + return Response::plaintext( "Book not found\n" - ); + )->withStatus(Response::STATUS_NOT_FOUND); } $data = $book->title; - return new Response( - 200, - [], + return Response::plaintext( $data ); }); diff --git a/examples/index.php b/examples/index.php index 3cc7546..5f87b49 100644 --- a/examples/index.php +++ b/examples/index.php @@ -8,9 +8,7 @@ $app = new FrameworkX\App(); $app->get('/', function () { - return new React\Http\Message\Response( - 200, - [], + return React\Http\Message\Response::plaintext( "Hello world!\n" ); }); @@ -21,21 +19,13 @@ return htmlspecialchars_decode(htmlspecialchars($str, ENT_SUBSTITUTE | ENT_DISALLOWED, 'utf-8')); }; - return new React\Http\Message\Response( - 200, - [ - 'Content-Type' => 'text/plain; charset=utf-8' - ], + return React\Http\Message\Response::plaintext( "Hello " . $escape($request->getAttribute('name')) . "!\n" ); }); $app->get('/uri[/{path:.*}]', function (ServerRequestInterface $request) { - return new React\Http\Message\Response( - 200, - [ - 'Content-Type' => 'text/plain' - ], + return React\Http\Message\Response::plaintext( (string) $request->getUri() . "\n" ); }); @@ -45,7 +35,7 @@ // Note that this assumes UTF-8 data in query params 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, + React\Http\Message\Response::STATUS_OK, [ 'Content-Type' => 'application/json' ], @@ -62,11 +52,7 @@ $info = htmlspecialchars($info, 0, 'utf-8'); } - return new React\Http\Message\Response( - 200, - [ - 'Content-Type' => 'text/html;charset=utf-8' - ], + return React\Http\Message\Response::html( '

Request

' . $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"