Leitfaden

Formulare & POST-Ablauf

Der typische Ablauf von einem GET-Formular über POST, CSRF und Validierung bis zum Redirect und anschließenden GET.

Vollständiger Formularablauf

Ein typischer serverseitig gerenderter Ablauf sieht so aus: GET rendert das Formular, POST läuft durch CsrfMiddleware, und der Controller liest und validiert die Eingabe genau einmal. Bei Fehlern liefert er das Formular mit Fehlern und ursprünglichen Werten zurück; bei Erfolg delegiert er den Schreibvorgang an einen Anwendungsservice, setzt eine Flash-Nachricht, leitet weiter und endet in einem anschließenden GET. Das ist das Post/Redirect/Get-Muster. Für detailliertere Validierungsregeln geht es mit Validierung weiter, für weitere HTTP-Guards mit Middleware.

Routen und CSRF-Schutz

Halte die GET-Route einfach und hänge CsrfMiddleware an die zustandsändernde POST-Route. Das View rendert das Token über den eingebauten CSRF-Helper, sodass Request und View denselben Framework-Mechanismus verwenden.

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);
};

GET-Action, POST-Action, Validierung und Redirect

Die GET-Action bereitet Standardwerte vor und rendert die Seite. Die POST-Action liest Eingaben über Controller-Helper, validiert einmal, liefert bei Fehlern dasselbe Formular mit HTTP 422 zurück und ruft bei Erfolg einen Anwendungsservice für den Schreibvorgang auf, setzt eine Flash-Nachricht und leitet weiter.

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,
        );
    }
}

Formular mit CSRF, Werten und Fehlern rendern

Das Form View rendert csrfField(), schreibt übermittelte Werte zurück in die Inputs und liest auf dem umgeleiteten GET die Success-Flash-Nachricht. Übergib values und errors explizit vom Controller, damit das View nicht von versteckten Helpern der Demo-Anwendung abhängt.

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>

Schreibvorgänge aus dem Controller heraushalten

Der Controller sollte die HTTP-Orchestrierung übernehmen. Der eigentliche Schreibvorgang kann in einem kleinen Anwendungsservice leben, sobald es um mehr als einfaches Request-Wiring geht. So bleibt der Controller lesbar, ohne Service- oder Repository-Schichten verpflichtend zu machen.

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);
    }
}