Skip to content

This chapter needs an extra package

composer require naf/form

Also installs these transitive dependencies: naf/session.

Handling a POST request

Every form, every API call that changes something, arrives the same way: a request with a body, which you read, check, act on, and answer. This page explains the flow; the linked recipes supply complete files. Start with Your first application, including its form dependency update and bootstrap correction for the released redirect helper.

One route or two

A form that renders and submits at the same URL needs both methods registered:

use App\Controllers\ContactController;
use function Naf\route;

route()->add('GET',  '/contact', [ContactController::class, 'show'],   'contact');
route()->add('POST', '/contact', [ContactController::class, 'submit'], 'contact.submit');

Two methods, two names, one path. Naming them separately matters because route('contact') is what your form's action resolves to. Both names produce the same URL here; the HTTP method selects the handler.

You can also point both at one method and branch inside it with is_post(). Two methods is usually easier to read; one method is easier when the form and its handling are three lines.

Reading the body

param() is the one to reach for. It merges the query string, the form body and — when the request says Content-Type: application/json and the parsed body is empty — the decoded JSON body, so the same controller reads a browser form and an API client without caring which it got:

use function Naf\param;

$email = param()->get('email');
$city  = param()->get('address.city', 'unknown');   // dotted path into nested data
$all   = param()->all();

The default is returned when the key is absent, so get() never surprises you with a notice.

request()->getParsedBody() is still there when you want the body and nothing else — no query parameters merged in. For JSON, decode (string) request()->getBody() explicitly instead; see Requests and responses.

Checking it

use function Naf\Form\validator;
use function Naf\param;

$check = validator()->validate(param()->all(), [
    'email'   => 'required|email',
    'message' => 'required|min:10',
]);

if (!$check->isValid()) {
    // $check->getErrorMessages() is ['email' => ['Please enter a valid email address.'], …]
}

Rules are a |-separated string, or an array. Five ship with naf/formrequired, email, min, max and boolean — and anything else you register yourself; see Forms and validation.

An unknown rule throws. validate() does not skip a rule it does not recognise, which is what you want: a typo in a rule name is a hole in your validation, and it should stop the request rather than quietly pass everything.

CSRF is already handled

With naf/form installed, a listener checks POST, PUT and DELETE before your controller runs. You put the token in the form, and that is the whole of your part:

<?php use function Naf\Form\csrf; ?>
<input type="hidden" name="_csrf" value="<?= csrf()->generate() ?>">

Bearer requests skip the CSRF check automatically; the endpoint still has to verify their credentials. Explicit named exemptions are available for other protocol endpoints. Forms and validation covers how.

Answering

After a successful browser form submission, redirect:

use function Naf\redirect;
use function Naf\route;

return redirect(route('contact'));

A rendered POST response is a page the browser will re-submit when somebody reloads it — the double-charge, double-mail, double-comment bug. Redirecting means the reload re-runs a GET.

Failure is the exception: when validation fails you do render, because the page has to come back with the errors and what was typed still in it. Nothing was changed, so there is nothing to re-submit.

Where to go next