scafera/form

Form handling and validation for the Scafera framework

Maintainers

Package info

github.com/scafera/form

Type:symfony-bundle

pkg:composer/scafera/form

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-04-16 12:01 UTC

This package is auto-updated.

Last update: 2026-04-16 12:13:38 UTC


README

Form handling and validation for the Scafera framework. DTO-based forms with attribute validation, CSRF protection, and typed value handling.

Internally adopts symfony/form, symfony/validator, and symfony/security-csrf. Userland code never imports Symfony Form or Validator types — boundary enforcement blocks it at compile time.

Provides: Form handling and validation for Scafera — DTO classes with Rule\* attributes, handled by FormHandler (web path, CSRF + old values) or Validator (standalone / API path). CSRF tokens scoped per DTO class (ADR-061). No Symfony FormType in userland (ADR-059).

Depends on: A Scafera host project. For file uploads, compose with scafera/file in the controller (ADR-062) — form and file are intentionally separate capabilities.

Extension points:

  • Attributes — Rule\NotBlank, MaxLength, MinLength, Min, Max, Positive, OneOf, Email, FileExtension on DTO properties
  • Controlled zone — src/Form/ allows Symfony AbstractType classes for complex forms that exceed DTO capabilities (same pattern as src/Repository/ for Doctrine)
  • Supported DTO property types — string, int, float, bool, DateTimeImmutable, BackedEnum, nullable variants

Not responsible for: File uploads (compose with scafera/file, ADR-062) · form themes or form_widget() (plain HTML only) · persistence (form is input-only) · direct use of Symfony\Component\Form or Symfony\Component\Validator outside src/Form/ (blocked by FormBoundaryPass and FormBoundaryValidator).

This is a capability package. It adds optional form handling and input validation to a Scafera project. It does not define folder structure or architectural rules — those belong to architecture packages.

What it provides

  • Validator — standalone DTO validation (API path)
  • FormHandler — form creation, submission, CSRF, and validation in one flow (web path)
  • Form — result object with errors, old values, and CSRF token
  • Rule\* attributes — declarative validation on DTO properties
  • DtoHydrator — internal DTO mapping with type transformation (nullable, enum, DateTimeImmutable)
  • src/Form/ controlled zone — Symfony Form types for complex forms

Design decisions

  • DTOs with attributes, not Symfony FormType — the common case uses plain PHP classes with Rule\* attributes. No Symfony types in userland (ADR-059).
  • CSRF scoped per form identity — each DTO class gets its own CSRF token. A token from one form cannot submit another (ADR-061).
  • File uploads handled separately — use scafera/file alongside FormHandler in your controller (ADR-062).
  • src/Form/ is a controlled zone, not an escape hatch — Symfony Form types are allowed in this directory only, following the same pattern as src/Repository/ for Doctrine.
  • Type transformation on old valuesold() returns typed values (int, enum, null), not raw strings. Templates render correctly without manual casting.

Installation

composer require scafera/form

Requirements

  • PHP >= 8.4
  • scafera/kernel

Input DTO

use Scafera\Form\Rule;

final class CreatePostInput
{
    #[Rule\NotBlank]
    #[Rule\MaxLength(200)]
    public string $title = '';

    #[Rule\MaxLength(5000)]
    public ?string $body = null;

    #[Rule\Positive]
    public int $priority = 1;
}

Supported property types: string, int, float, bool, DateTimeImmutable, nullable variants, and BackedEnum.

Available rules

Rule Parameters
#[NotBlank]
#[MaxLength(int)] max characters
#[MinLength(int)] min characters
#[Min(int|float)] minimum value
#[Max(int|float)] maximum value
#[Positive] must be > 0
#[OneOf(array)] allowed values
#[Email] email format
#[FileExtension(array)] allowed file extensions

Standalone validation (API path)

use Scafera\Form\Validator;

$input = new CreatePostInput();
$input->title = '';

$result = $validator->validate($input);
$result->hasErrors();      // true
$result->firstError();     // ValidationError{field: 'title', message: '...'}
$result->toArray();        // ['title' => ['This value should not be blank.']]

Form handling (web path)

use Scafera\Form\FormHandler;

#[Route('/posts/new', methods: ['GET', 'POST'])]
final class CreatePost
{
    public function __construct(
        private readonly FormHandler $form,
        private readonly ViewInterface $view,
    ) {}

    public function __invoke(Request $request): ResponseInterface
    {
        $form = $this->form->handle($request, CreatePostInput::class);

        if ($form->isValid()) {
            $input = $form->getData();  // typed CreatePostInput
            // persist...
            return new RedirectResponse('/posts');
        }

        return new Response($this->view->render('posts/new.html.twig', [
            'form' => $form,
        ]));
    }
}

Template

Plain HTML. No form themes, no form_widget():

<form method="POST">
    <input type="hidden" name="_csrf" value="{{ form.csrfToken() }}">

    <label>Title</label>
    <input name="title" value="{{ form.old('title') }}">
    {% if form.error('title') %}
        <span class="error">{{ form.error('title') }}</span>
    {% endif %}

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

Form API

Method Returns
isSubmitted() bool
isValid() bool (true only when submitted + valid)
getData() typed input DTO
errors() array<string, list<string>>
error(string $field) ?string (first error for field)
old(string $field) mixed (submitted value, type-transformed)
csrfToken() string

Controlled zone: src/Form/

For forms that exceed DTO capabilities (nested forms, dynamic fields, entity-backed choices), place Symfony AbstractType classes in src/Form/:

src/
  Form/            ← Symfony Form imports allowed here
    OrderForm.php

This is a controlled zone — the same pattern as src/Repository/ for Doctrine. Symfony Form and Validator imports are allowed in this directory and blocked everywhere else.

Rules for src/Form/:

  • May reference App\Entity for type hints and structure — must NOT perform persistence or lifecycle operations
  • Must NOT import from App\Service, App\Repository, App\Controller, App\Command, or App\Integration
  • Only controllers may import from App\Form — services must NOT depend on forms
  • You are responsible for wiring Form classes into your controller via Symfony's FormFactory directly

Boundary enforcement

Blocked Use instead
Symfony\Component\Form\* Scafera\Form\FormHandler
Symfony\Component\Validator\* Scafera\Form\Validator + Rule\*

Allowed inside src/Form/ only (controlled zone). Enforced via compiler pass (build time) and validator (scafera validate). Detects use, new, and extends patterns.

License

MIT