Guida

Form e flusso POST

Il flusso comune da un form GET attraverso POST, CSRF e validazione fino al redirect e al GET successivo.

Flusso completo di un form

Un tipico flusso server-rendered funziona così: GET renderizza il form, POST passa attraverso CsrfMiddleware e il controller legge e valida l’input una sola volta. In caso di errore restituisce il form con errori e valori originali; in caso di successo delega la scrittura a un servizio applicativo, salva un flash message, esegue il redirect e termina con un GET successivo. È il pattern Post/Redirect/Get. Continua con Validazione per le rules e con Middleware per altri guard HTTP.

Route e protezione CSRF

Mantieni semplice la route GET e collega CsrfMiddleware alla route POST che modifica lo stato. La view renderizza il token tramite l’helper CSRF integrato, così request e view usano lo stesso meccanismo del framework.

example.php
<?php

use App\Controllers\ContactController;
use Lemonade\Framework\Routing\Router;
use Lemonade\Framework\Security\Csrf\CsrfMiddleware;

return static function (Router $router): void {
    $router->getNamed('contact.form', '/contact', ContactController::class . '@form');

    $router
        ->postNamed('contact.submit', '/contact', ContactController::class . '@submit')
        ->middleware(CsrfMiddleware::class);
};

Azione GET, azione POST, validazione e redirect

L’azione GET prepara i valori iniziali e renderizza la pagina. L’azione POST legge l’input tramite gli helper del controller, lo valida una sola volta, restituisce lo stesso form con HTTP 422 in caso di errore e, in caso di successo, chiama un servizio applicativo per la scrittura, imposta un flash message ed esegue il redirect.

example.php
<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Services\ContactSubmissionService;
use Lemonade\Framework\Core\AbstractController;
use Lemonade\Framework\Validation\ValidationSchema;
use Psr\Http\Message\ResponseInterface;

final class ContactController extends AbstractController
{
    public function __construct(
        private readonly ContactSubmissionService $contactService,
    ) {}

    public function form(): ResponseInterface
    {
        return $this->renderForm(
            values: [
                'name' => '',
                'email' => '',
                'message' => '',
            ],
            errors: [],
        );
    }

    public function submit(): ResponseInterface
    {
        $values = [
            'name' => $this->inputString('name'),
            'email' => $this->inputString('email'),
            'message' => $this->inputString('message'),
        ];

        $schema = ValidationSchema::create()
            ->field('name', 'Name')
                ->required()
                ->maxLength(100)
            ->end()
            ->field('email', 'E-mail')
                ->required()
                ->email()
            ->end()
            ->field('message', 'Message')
                ->required()
                ->maxLength(2000)
            ->end();

        $result = $this->validator()->validate($values, $schema);

        if (!$result->isValid()) {
            return $this->renderForm(
                values: $values,
                errors: $result->errors(),
                status: 422,
            );
        }

        $this->contactService->store($result->validated());
        $this->flash()->set('success', 'Your message has been sent.');

        return $this->redirect($this->url()->route('contact.form'));
    }

    /**
     * @param array{name:string,email:string,message:string} $values
     * @param array<string, string> $errors
     */
    private function renderForm(array $values, array $errors, int $status = 200): ResponseInterface
    {
        return $this->html(
            $this->view()->template('layouts.app', 'pages.contact-form', [
                'values' => $values,
                'errors' => $errors,
            ]),
            $status,
        );
    }
}

Renderizza il form con CSRF, valori ed errori

La form view renderizza csrfField(), rimette i valori inviati negli input e legge il success flash message sul GET dopo il redirect. Passa values ed errors esplicitamente dal controller così la view non dipende da helper nascosti dell’applicazione demo.

example.php
<?php

/**
 * @var \Lemonade\Framework\View\View $this
 * @var \Lemonade\Framework\View\ViewHelpers $helpers
 * @var \Lemonade\Framework\View\RequestViewHelpers $requestHelpers
 * @var array<string, string> $errors
 * @var array{name:string,email:string,message:string} $values
 */
?>
<?php if ($success = $requestHelpers->flash('success')): ?>
    <div class="alert alert-success"><?= e((string) $success) ?></div>
<?php endif; ?>

<form method="post" action="<?= e($helpers->url('contact.submit')) ?>">
    <?= $helpers->csrfField() ?>

    <input name="name" value="<?= e($values['name']) ?>">
    <input name="email" value="<?= e($values['email']) ?>">
    <textarea name="message"><?= e($values['message']) ?></textarea>

    <?php if (isset($errors['message'])): ?>
        <p><?= e($errors['message']) ?></p>
    <?php endif; ?>

    <button type="submit">Send</button>
</form>

Tieni la scrittura fuori dal controller

Il controller dovrebbe gestire l’orchestrazione HTTP. La scrittura può vivere in un piccolo servizio applicativo quando non è più un semplice wiring tra request e framework. In questo modo il controller resta leggibile senza rendere obbligatori gli strati service o repository.

example.php
<?php

declare(strict_types=1);

namespace App\Services;

use Lemonade\Framework\Database\Database;

final class ContactSubmissionService
{
    public function __construct(
        private readonly Database $db,
    ) {}

    /**
     * @param array{name:string,email:string,message:string} $data
     */
    public function store(array $data): void
    {
        $this->db->table('contact_messages')->insert($data);
    }
}