Skip to content

Add file upload example and documentation #252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 57 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -722,56 +722,71 @@ $middlewares = new MiddlewareRunner([

#### RequestBodyParserMiddleware

The `RequestBodyParserMiddleware` takes a fully buffered request body (generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)),
and parses the forms and uploaded files from the request body.

Parsed submitted forms will be available from `$request->getParsedBody()` as
array.
For example the following submitted body (`application/x-www-form-urlencoded`):

`bar[]=beer&bar[]=wine`

Results in the following parsed body:
The `RequestBodyParserMiddleware` takes a fully buffered request body
(generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)),
and parses the form values and file uploads from the incoming HTTP request body.

This middleware handler takes care of applying values from HTTP
requests that use `Content-Type: application/x-www-form-urlencoded` or
`Content-Type: multipart/form-data` to resemble PHP's default superglobals
`$_POST` and `$_FILES`.
Instead of relying on these superglobals, you can use the
`$request->getParsedBody()` and `$request->getUploadedFiles()` methods
as defined by PSR-7.

Accordingly, each file upload will be represented as instance implementing [`UploadedFileInterface`](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#36-psrhttpmessageuploadedfileinterface).
Due to its blocking nature, the `moveTo()` method is not available and throws
a `RuntimeException` instead.
You can use `$contents = (string)$file->getStream();` to access the file
contents and persist this to your favorite data store.

```php
$parsedBody = [
'bar' => [
'beer',
'wine',
],
];
```

Aside from `application/x-www-form-urlencoded`, this middleware handler
also supports `multipart/form-data`, thus supporting uploaded files available
through `$request->getUploadedFiles()`.

The `$request->getUploadedFiles(): array` will return an array with all
uploaded files formatted like this:

```php
$uploadedFiles = [
'avatar' => new UploadedFile(/**...**/),
'screenshots' => [
new UploadedFile(/**...**/),
new UploadedFile(/**...**/),
],
];
```
$handler = function (ServerRequestInterface $request) {
// If any, parsed form fields are now available from $request->getParsedBody()
$body = $request->getParsedBody();
$name = isset($body['name']) ? $body['name'] : 'unnamed';

$files = $request->getUploadedFiles();
$avatar = isset($files['avatar']) ? $files['avatar'] : null;
if ($avatar instanceof UploadedFileInterface) {
if ($avatar->getError() === UPLOAD_ERR_OK) {
$uploaded = $avatar->getSize() . ' bytes';
} else {
$uploaded = 'with error';
}
} else {
$uploaded = 'nothing';
}

Usage:
return new Response(
200,
array(
'Content-Type' => 'text/plain'
),
$name . ' uploaded ' . $uploaded
);
};

```php
$middlewares = new MiddlewareRunner([
$server = new Server(new MiddlewareRunner([
new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
new RequestBodyParserMiddleware(),
function (ServerRequestInterface $request, callable $next) {
// If any, parsed form fields are now available from $request->getParsedBody()
return new Response(200);
},
]);
$handler
]));
```

See also [example #12](examples) for more details.

> Note that this middleware handler simply parses everything that is already
buffered in the request body.
It is imperative that the request body is buffered by a prior middleware
handler as given in the example above.
This previous middleware handler is also responsible for rejecting incoming
requests that exceed allowed message sizes (such as big file uploads).
If you use this middleware without buffering first, it will try to parse an
empty (streaming) body and may thus assume an empty data structure.
See also [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) for
more details.

#### Third-Party Middleware

A non-exhaustive list of third-party middleware can be found at the [`Middleware`](https://github.com/reactphp/http/wiki/Middleware) wiki page.
Expand Down
132 changes: 132 additions & 0 deletions examples/12-upload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

// Simple HTML form with file upload
// Launch demo and use your favorite browser or CLI tool to test form submissions
//
// $ php examples/12-upload.php 8080
// $ curl --form name=test --form age=30 http://localhost:8080/
// $ curl --form name=hi --form avatar=@avatar.png http://localhost:8080/

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use React\EventLoop\Factory;
use React\Http\MiddlewareRunner;
use React\Http\Middleware\RequestBodyBufferMiddleware;
use React\Http\Middleware\RequestBodyParserMiddleware;
use React\Http\Response;
use React\Http\Server;

require __DIR__ . '/../vendor/autoload.php';

$loop = Factory::create();

$handler = function (ServerRequestInterface $request) {
if ($request->getMethod() === 'POST') {
// Take form input values from POST values (for illustration purposes only!)
// Does not actually validate data here
$body = $request->getParsedBody();
$name = isset($body['name']) && is_string($body['name']) ? htmlspecialchars($body['name']) : 'n/a';
$age = isset($body['age']) && is_string($body['age']) ? (int)$body['age'] : 'n/a';

// Show uploaded avatar as image (for illustration purposes only!)
// Real applications should validate the file data to ensure this is
// actually an image and not rely on the client media type.
$avatar = 'n/a';
$uploads = $request->getUploadedFiles();
if (isset($uploads['avatar']) && $uploads['avatar'] instanceof UploadedFileInterface) {
/* @var $file UploadedFileInterface */
$file = $uploads['avatar'];
if ($file->getError() === UPLOAD_ERR_OK) {
// Note that moveFile() is not available due to its blocking nature.
// You can use your favorite data store to simply dump the file
// contents via `(string)$file->getStream()` instead.
// Here, we simply use an inline image to send back to client:
$avatar = '<img src="data:'. $file->getClientMediaType() . ';base64,' . base64_encode($file->getStream()) . '" /> (' . $file->getSize() . ' bytes)';
} else {
// Real applications should probably check the error number and
// should print some human-friendly text
$avatar = 'upload error ' . $file->getError();
}
}

$dump = htmlspecialchars(
var_export($request->getParsedBody(), true) .
PHP_EOL .
var_export($request->getUploadedFiles(), true)
);

$body = <<<BODY
Name: $name
Age: $age
Avatar $avatar

<pre>
$dump
</pre>
BODY;
} else {
$body = <<<BODY
<form method="POST" enctype="multipart/form-data">
<label>
Your name
<input type="text" name="name" required />
</label>

<label>
Your age
<input type="number" name="age" min="9" max="99" required />
</label>

<label>
Upload avatar (optional, 100KB max)
<input type="file" name="avatar" />
</label>

<button type="submit">
» Submit
</button>
</form>
BODY;
}

$html = <<<HTML
<!DOCTYPE html>
<html>
<head>
<style>
body{
background-color: #eee;
color: #aaa;
}
label{
display: block;
margin-bottom: .5em;
}
</style>
<body>
$body
</body>
</html>

HTML;

return new Response(
200,
array('Content-Type' => 'text/html; charset=UTF-8'),
$html
);
};

// buffer and parse HTTP request body before running our request handler
$server = new Server(new MiddlewareRunner(array(
new RequestBodyBufferMiddleware(100000), // 100 KB max
new RequestBodyParserMiddleware(),
$handler
)));

$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop);
$server->listen($socket);

echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL;

$loop->run();