Skip to content

Commit 19fd073

Browse files
committed
Add file upload example and documentation
1 parent 1d5bd0b commit 19fd073

File tree

2 files changed

+189
-42
lines changed

2 files changed

+189
-42
lines changed

README.md

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -722,56 +722,71 @@ $middlewares = new MiddlewareRunner([
722722

723723
#### RequestBodyParserMiddleware
724724

725-
The `RequestBodyParserMiddleware` takes a fully buffered request body (generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)),
726-
and parses the forms and uploaded files from the request body.
727-
728-
Parsed submitted forms will be available from `$request->getParsedBody()` as
729-
array.
730-
For example the following submitted body (`application/x-www-form-urlencoded`):
731-
732-
`bar[]=beer&bar[]=wine`
733-
734-
Results in the following parsed body:
725+
The `RequestBodyParserMiddleware` takes a fully buffered request body
726+
(generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)),
727+
and parses the form values and file uploads from the incoming HTTP request body.
728+
729+
This middleware handler takes care of applying values from HTTP
730+
requests that use `Content-Type: application/x-www-form-urlencoded` or
731+
`Content-Type: multipart/form-data` to resemble PHP's default superglobals
732+
`$_POST` and `$_FILES`.
733+
Instead of relying on these superglobals, you can use the
734+
`$request->getParsedBody()` and `$request->getUploadedFiles()` methods
735+
as defined by PSR-7.
736+
737+
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).
738+
Due to its blocking nature, the `moveTo()` method is not available and throws
739+
a `RuntimeException` instead.
740+
You can use `$contents = (string)$file->getStream();` to access the file
741+
contents and persist this to your favorite data store.
735742

736743
```php
737-
$parsedBody = [
738-
'bar' => [
739-
'beer',
740-
'wine',
741-
],
742-
];
743-
```
744-
745-
Aside from `application/x-www-form-urlencoded`, this middleware handler
746-
also supports `multipart/form-data`, thus supporting uploaded files available
747-
through `$request->getUploadedFiles()`.
748-
749-
The `$request->getUploadedFiles(): array` will return an array with all
750-
uploaded files formatted like this:
751-
752-
```php
753-
$uploadedFiles = [
754-
'avatar' => new UploadedFile(/**...**/),
755-
'screenshots' => [
756-
new UploadedFile(/**...**/),
757-
new UploadedFile(/**...**/),
758-
],
759-
];
760-
```
744+
$handler = function (ServerRequestInterface $request) {
745+
// If any, parsed form fields are now available from $request->getParsedBody()
746+
$body = $request->getParsedBody();
747+
$name = isset($body['name']) ? $body['name'] : 'unnamed';
748+
749+
$files = $request->getUploadedFiles();
750+
$avatar = isset($files['avatar']) ? $files['avatar'] : null;
751+
if ($avatar instanceof UploadedFileInterface) {
752+
if ($avatar->getError() === UPLOAD_ERR_OK) {
753+
$uploaded = $avatar->getSize() . ' bytes';
754+
} else {
755+
$uploaded = 'with error';
756+
}
757+
} else {
758+
$uploaded = 'nothing';
759+
}
761760

762-
Usage:
761+
return new Response(
762+
200,
763+
array(
764+
'Content-Type' => 'text/plain'
765+
),
766+
$name . ' uploaded ' . $uploaded
767+
);
768+
};
763769

764-
```php
765-
$middlewares = new MiddlewareRunner([
770+
$server = new Server(new MiddlewareRunner([
766771
new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
767772
new RequestBodyParserMiddleware(),
768-
function (ServerRequestInterface $request, callable $next) {
769-
// If any, parsed form fields are now available from $request->getParsedBody()
770-
return new Response(200);
771-
},
772-
]);
773+
$handler
774+
]));
773775
```
774776

777+
See also [example #12](examples) for more details.
778+
779+
> Note that this middleware handler simply parses everything that is already
780+
buffered in the request body.
781+
It is imperative that the request body is buffered by a prior middleware
782+
handler as given in the example above.
783+
This previous middleware handler is also responsible for rejecting incoming
784+
requests that exceed allowed message sizes (such as big file uploads).
785+
If you use this middleware without buffering first, it will try to parse an
786+
empty (streaming) body and may thus assume an empty data structure.
787+
See also [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) for
788+
more details.
789+
775790
#### Third-Party Middleware
776791

777792
A non-exhaustive list of third-party middleware can be found at the [`Middleware`](https://github.com/reactphp/http/wiki/Middleware) wiki page.

examples/12-upload.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
// Simple HTML form with file upload
4+
// Launch demo and use your favorite browser or CLI tool to test form submissions
5+
//
6+
// $ php examples/12-upload.php 8080
7+
// $ curl --form name=test --form age=30 http://localhost:8080/
8+
// $ curl --form name=hi --form [email protected] http://localhost:8080/
9+
10+
use Psr\Http\Message\ServerRequestInterface;
11+
use Psr\Http\Message\UploadedFileInterface;
12+
use React\EventLoop\Factory;
13+
use React\Http\MiddlewareRunner;
14+
use React\Http\Middleware\RequestBodyBufferMiddleware;
15+
use React\Http\Middleware\RequestBodyParserMiddleware;
16+
use React\Http\Response;
17+
use React\Http\Server;
18+
19+
require __DIR__ . '/../vendor/autoload.php';
20+
21+
$loop = Factory::create();
22+
23+
$handler = function (ServerRequestInterface $request) {
24+
if ($request->getMethod() === 'POST') {
25+
// Take form input values from POST values (for illustration purposes only!)
26+
// Does not actually validate data here
27+
$body = $request->getParsedBody();
28+
$name = isset($body['name']) && is_string($body['name']) ? htmlspecialchars($body['name']) : 'n/a';
29+
$age = isset($body['age']) && is_string($body['age']) ? (int)$body['age'] : 'n/a';
30+
31+
// Show uploaded avatar as image (for illustration purposes only!)
32+
// Real applications should validate the file data to ensure this is
33+
// actually an image and not rely on the client media type.
34+
$avatar = 'n/a';
35+
$uploads = $request->getUploadedFiles();
36+
if (isset($uploads['avatar']) && $uploads['avatar'] instanceof UploadedFileInterface) {
37+
/* @var $file UploadedFileInterface */
38+
$file = $uploads['avatar'];
39+
if ($file->getError() === UPLOAD_ERR_OK) {
40+
// Note that moveFile() is not available due to its blocking nature.
41+
// You can use your favorite data store to simply dump the file
42+
// contents via `(string)$file->getStream()` instead.
43+
// Here, we simply use an inline image to send back to client:
44+
$avatar = '<img src="data:'. $file->getClientMediaType() . ';base64,' . base64_encode($file->getStream()) . '" /> (' . $file->getSize() . ' bytes)';
45+
} else {
46+
// Real applications should probably check the error number and
47+
// should print some human-friendly text
48+
$avatar = 'upload error ' . $file->getError();
49+
}
50+
}
51+
52+
$dump = htmlspecialchars(
53+
var_export($request->getParsedBody(), true) .
54+
PHP_EOL .
55+
var_export($request->getUploadedFiles(), true)
56+
);
57+
58+
$body = <<<BODY
59+
Name: $name
60+
Age: $age
61+
Avatar $avatar
62+
63+
<pre>
64+
$dump
65+
</pre>
66+
BODY;
67+
} else {
68+
$body = <<<BODY
69+
<form method="POST" enctype="multipart/form-data">
70+
<label>
71+
Your name
72+
<input type="text" name="name" required />
73+
</label>
74+
75+
<label>
76+
Your age
77+
<input type="number" name="age" min="9" max="99" required />
78+
</label>
79+
80+
<label>
81+
Upload avatar (optional, 100KB max)
82+
<input type="file" name="avatar" />
83+
</label>
84+
85+
<button type="submit">
86+
» Submit
87+
</button>
88+
</form>
89+
BODY;
90+
}
91+
92+
$html = <<<HTML
93+
<!DOCTYPE html>
94+
<html>
95+
<head>
96+
<style>
97+
body{
98+
background-color: #eee;
99+
color: #aaa;
100+
}
101+
label{
102+
display: block;
103+
margin-bottom: .5em;
104+
}
105+
</style>
106+
<body>
107+
$body
108+
</body>
109+
</html>
110+
111+
HTML;
112+
113+
return new Response(
114+
200,
115+
array('Content-Type' => 'text/html; charset=UTF-8'),
116+
$html
117+
);
118+
};
119+
120+
// buffer and parse HTTP request body before running our request handler
121+
$server = new Server(new MiddlewareRunner(array(
122+
new RequestBodyBufferMiddleware(100000), // 100 KB max
123+
new RequestBodyParserMiddleware(),
124+
$handler
125+
)));
126+
127+
$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop);
128+
$server->listen($socket);
129+
130+
echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL;
131+
132+
$loop->run();

0 commit comments

Comments
 (0)