Documentation · Chapter 04

Validate, explain,
then redirect.

Good validation does more than reject: it preserves input and clearly explains what must be fixed.

We continue the reading-list project. Data will be kept in the session before introducing a database.

Inject Validator and Session

Replace the beginning of BookController with these imports and constructor. The PHPAML container automatically supplies the requested objects.

phpaml — zsh
use PHPAML\Http\Request;
use PHPAML\Http\Response;
use PHPAML\Mvc\Controller;
use PHPAML\Mvc\View;
use PHPAML\Session\Session;
use PHPAML\Validation\Validator;

final class BookController extends Controller
{
    public function __construct(
        View $view,
        private Session $session,
        private Validator $validator,
    ) {
        parent::__construct($view);
    }

Validate before storing

phpaml — zsh
public function store(Request $request): Response
{
    $data = [
        'title' => trim((string) $request->input('title', '')),
        'author' => trim((string) $request->input('author', '')),
    ];

    $valid = $this->validator->validate($data, [
        'title' => ['required', 'string', 'min:2', 'max:120'],
        'author' => ['required', 'string', 'min:2', 'max:100'],
    ]);

    if (!$valid) {
        return $this->view('books/create.php', [
            'errors' => $this->validator->errors(),
            'old' => $data,
        ], 422);
    }

    $books = $this->session->get('books', []);
    $nextId = $books === [] ? 1 : max(array_column($books, 'id')) + 1;
    $books[] = ['id' => $nextId] + $data;
    $this->session->set('books', $books);

    return Response::redirect('/books', 303);
}

Explain failure near the field

Add this block immediately after the title field in create.php. Do the same for author.

phpaml — zsh
<?php foreach ($errors['title'] ?? [] as $error): ?>
    <p role="alert"><?= $view->escape($error) ?></p>
<?php endforeach; ?>

Status 422 means the request is understood but its values do not follow the rules. The view receives old so the user does not have to retype everything.

Why redirect with 303?

After saving, Response::redirect('/books', 303) tells the browser: “the operation is complete; now read the list with GET.” A refresh then repeats GET, not POST.

1POST/books
→
2303Location: /books
→
3GET/books