Formulaires et workflow POST
Le flux courant d’un formulaire GET vers POST, CSRF et validation, puis redirect et GET suivant.
Flux complet d’un formulaire
Un flux server-rendered classique fonctionne ainsi : GET rend le formulaire, POST passe par CsrfMiddleware, puis le controller lit et valide l’entrée une seule fois. En cas d’erreur, il retourne le formulaire avec les erreurs et les valeurs d’origine ; en cas de succès, il délègue l’écriture à un service applicatif, enregistre un flash message, effectue un redirect et se termine par un GET suivant. C’est le pattern Post/Redirect/Get. Consultez Validation pour les rules et Middleware pour d’autres guards HTTP.
Routes et protection CSRF
Gardez la route GET simple et attachez CsrfMiddleware à la route POST qui modifie l’état. La view rend le token via le helper CSRF intégré, de sorte que request et view utilisent le même mécanisme du framework.
<?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);
};Action GET, action POST, validation et redirect
L’action GET prépare les valeurs par défaut et rend la page. L’action POST lit les entrées via les helpers du controller, les valide une seule fois, retourne le même formulaire avec HTTP 422 en cas d’erreur et, en cas de succès, appelle un service applicatif pour l’écriture, définit un flash message et redirige.
<?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,
);
}
}Rendre le formulaire avec CSRF, valeurs et erreurs
La form view rend csrfField(), remet les valeurs soumises dans les inputs et lit le success flash message sur le GET après redirect. Transmettez values et errors explicitement depuis le controller afin que la view ne dépende pas de helpers cachés de l’application de démonstration.
<?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>Garder l’écriture hors du controller
Le controller doit gérer l’orchestration HTTP. L’écriture elle-même peut vivre dans un petit service applicatif dès qu’elle dépasse un simple wiring entre request et framework. Le controller reste ainsi lisible sans rendre les couches service ou repository obligatoires.
<?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);
}
}