amphp/http-server-form-parser
Parses application/x-www-form-urlencoded and multipart/form-data request bodies for amphp/http-server, giving handlers easy access to submitted form fields and uploaded files with a simple, asynchronous API.
Install alongside an existing amphp/http-server v3 application:
composer require amphp/http-server-form-parser
You need PHP 8.1+ and amphp/http-server v3. The package is tiny; start by reading src/Form.php, src/File.php, and src/StreamingFormParser.php. The public surface is essentially three things: a parser you instantiate, a Form value object, and File objects for uploads.
A first handler looks like this:
use Amp\Http\Server\FormParser\StreamingFormParser;
use Amp\Http\Server\Request;
use Amp\Http\Server\Response;
use Amp\Http\Server\RequestHandler\ClosureRequestHandler;
$formParser = new StreamingFormParser();
$router->addRoute('POST', '/contact', new ClosureRequestHandler(
function (Request $request) use ($formParser): Response {
$form = $formParser->parseRequest($request);
$name = $form->getValue('name');
$email = $form->getValue('email');
if ($name === null || $email === null) {
return new Response(422, [], 'Missing fields');
}
return new Response(200, [], "Thanks, {$name}");
}
));
The first realistic use case is a contact/login/upload form: parse application/x-www-form-urlencoded or multipart/form-data, pull scalar values with getValue() and uploaded files with getFile(). Older releases exposed Form::fromRequest($request); if you are on a pre-v2 version, check the README and use that entry point instead.
Create the parser once, reuse it. Instantiate StreamingFormParser at application boot and inject it into handlers or a shared container. It is stateless per request.
Parse per request, not globally. Call parseRequest($request) at the top of the handler that owns form handling. Avoid a blanket middleware that parses every request; most routes don’t need it and body parsing can be expensive.
Single value vs. multi-value. Use getValue('field') for the common case. When a form can repeat a name (checkboxes, multi-selects), use getFields()['field'] ?? [] and iterate. Uploads follow then same shape: getFiles()['attachments'] ?? [], each item a File.
Stream uploads to disk instead of buffering. File::getContents() returns a ReadableStream. For anything larger than trivial, pipe it straight to an Amp\File\WritableFile or object storage rather than calling buffer():
$file = $form->getFile('avatar');
if ($file !== null) {
$target = Amp\File\openFile($path, 'w');
Amp\ByteStream\pipe($file->getContents(), $target);
}
Keep validation in your own layer. The parser gives you strings and streams, not typed data. Convert to DTOs, run your validator, and only then touch the database or filesystem.
Compose with Router and ClosureRequestHandler/custom handlers. This package has no framework integration; it is a low-level primitive inside the Amp request lifecycle. The idiomatic flow is: router → handler → parser → validate → respond.
Testing pattern. Build a Request with a body stream containing the encoded form, invoke the handler, and assert on the Response. This is fast and avoids a live server. For multipart tests, generate bodies with curl once and store fixtures, or construct them manually with the correct boundary.
Use route-level guards. Parse form data only after method/content-type checks, e.g. reject GET and non-form Content-Type before invoking the parser.
$request->getBody(), the form parser will see an empty or broken body. Parse once and pass the resulting Form around, not the Request.amphp/http-server commonly runs with ClientBodySizeMiddleware; multipart uploads that exceed it will be rejected before your handler sees them. Tune or disable that middleware deliberately, and add your own file-size checks.File::getContents() is backed by the request body. If you return a response and try to read the stream later (background job, queued upload), you may get a closed stream. Copy or persist the file inside the handler.getValue() to return the final occurrence can be surprised. Always check getFields() when names may repeat.user[name] remains the literal key user[name]; there is no PHP-style array expansion. Flatten in your application or use explicit field names.multipart/form-data with a missing/mismatched boundary, or an unsupported content type, will produce an empty form or a parse exception depending on version. Catch parse failures and return 400 rather than letting them bubble to a 500.File::getName() and getMimeType() come from the client. Never use the raw filename as a destination path; generate your own and validate MIME by sniffing content.multipart parsing or a different framework integration) for edge cases.Content-Type, Content-Length, and whether the body stream was already consumed. Reproduce with curl -F and curl --data-urlencode separately; URL-encoded and multipart paths fail differently.amphp/http-server middleware (body limits, timeouts, compression). Treat the parser as a narrow utility, not a form framework.How can I help you explore Laravel packages today?