Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Forms and validation

A form needs two routes: GET shows the empty form, POST receives the submitted data. Data from a visitor cannot be trusted, so it is validated before saving. Slim has no built-in validation — you write it yourself or add a library.

The form

{# templates/posts/new.twig #}
{% extends 'layout.twig' %}
 
{% block content %}
    <h1>New post</h1>
 
    <form method="post" action="{{ url_for('posts.store') }}">
        <input type="text" name="title" placeholder="Title">
        <textarea name="body" placeholder="Text"></textarea>
        <button type="submit">Save</button>
    </form>
{% endblock %}

The action that shows it:

// src/Action/NewPostAction.php
final class NewPostAction
{
    public function __construct(private Twig $twig) {}
 
    public function __invoke(Request $request, Response $response): Response
    {
        return $this->twig->render($response, 'posts/new.twig');
    }
}

The namespace and use lines are left out here and below — they are the same as in earlier actions.

Routes

// config/routes.php
$app->get('/posts/new', NewPostAction::class)->setName('posts.new');
$app->post('/posts', CreatePostAction::class)->setName('posts.store');

The /posts/new address opens the form. The button leads to an error for now: the second action does not exist yet.

Validation

Validation rules go into a separate class in the src/Validation/ folder. It receives the data and returns an array of errors — empty when everything is fine:

// src/Validation/PostValidator.php
namespace App\Validation;
 
final class PostValidator
{
    public function validate(array $data): array
    {
        $errors = [];
 
        if (mb_strlen(trim($data['title'] ?? '')) < 5) {
            $errors['title'] = 'The title must be at least 5 characters long';
        }
 
        if (trim($data['body'] ?? '') === '') {
            $errors['body'] = 'Enter the post text';
        }
 
        return $errors;
    }
}

Receiving the data

// src/Action/CreatePostAction.php
final class CreatePostAction
{
    public function __construct(
        private PostRepository $posts,
        private PostValidator $validator,
        private Twig $twig,
    ) {}
 
    public function __invoke(Request $request, Response $response): Response
    {
        $data = (array) $request->getParsedBody();
        $errors = $this->validator->validate($data);
 
        if ($errors !== []) {
            return $this->twig->render($response->withStatus(422), 'posts/new.twig', [
                'errors' => $errors,
                'old' => $data,
            ]);
        }
 
        $this->posts->create(trim($data['title']), trim($data['body']));
 
        $url = RouteContext::fromRequest($request)->getRouteParser()->urlFor('posts.index');
 
        return $response->withHeader('Location', $url)->withStatus(302);
    }
}

If there are errors, the form is rendered again with code 422 — "the data did not pass validation". The errors and the entered values are passed to the template. If there are none, the post is saved and the browser is redirected to the list.

Printing errors

Extend the template: fill in the previous values and the error messages.

{# templates/posts/new.twig #}
<input type="text" name="title" value="{{ old.title }}" placeholder="Title">
{% if errors.title %}
    <p class="error">{{ errors.title }}</p>
{% endif %}
 
<textarea name="body" placeholder="Text">{{ old.body }}</textarea>
{% if errors.body %}
    <p class="error">{{ errors.body }}</p>
{% endif %}

Submit an empty form — messages appear under the fields. On the first visit the errors and old variables do not exist; Twig treats them as empty and prints nothing.

Validation libraries

When the rules multiply, a library replaces your own class, for example respect/validation or symfony/validator. Its place in the project is the same: a class in src/Validation/ called from the action.

Common mistake

If the browser is not redirected after a successful save and a page is rendered right away, reloading that page submits the form again — a second identical post appears in the database. After a successful POST always return a redirect.

Thanks for reading!

Donate to the site — we work hard for you.