PHPackages                             scafera/form - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Validation &amp; Sanitization](/categories/validation)
4. /
5. scafera/form

ActiveSymfony-bundle[Validation &amp; Sanitization](/categories/validation)

scafera/form
============

Form handling and validation for the Scafera framework

v1.0.1(1mo ago)02MITPHPPHP &gt;=8.4

Since Apr 14Pushed 1mo agoCompare

[ Source](https://github.com/scafera/form)[ Packagist](https://packagist.org/packages/scafera/form)[ Docs](https://github.com/scafera/form)[ RSS](/packages/scafera-form/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (2)Dependencies (5)Versions (3)Used By (0)

scafera/form
============

[](#scaferaform)

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
----------------

[](#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
----------------

[](#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 values** — `old()` returns typed values (int, enum, null), not raw strings. Templates render correctly without manual casting.

Installation
------------

[](#installation)

```
composer require scafera/form
```

Requirements
------------

[](#requirements)

- PHP &gt;= 8.4
- scafera/kernel

Input DTO
---------

[](#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
---------------

[](#available-rules)

RuleParameters`#[NotBlank]`—`#[MaxLength(int)]`max characters`#[MinLength(int)]`min characters`#[Min(int|float)]`minimum value`#[Max(int|float)]`maximum value`#[Positive]`must be &gt; 0`#[OneOf(array)]`allowed values`#[Email]`email format`#[FileExtension(array)]`allowed file extensionsStandalone validation (API path)
--------------------------------

[](#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)
------------------------

[](#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
--------

[](#template)

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

```

    Title

    {% if form.error('title') %}
        {{ form.error('title') }}
    {% endif %}

    Create

```

Form API
--------

[](#form-api)

MethodReturns`isSubmitted()``bool``isValid()``bool` (true only when submitted + valid)`getData()`typed input DTO`errors()``array``error(string $field)``?string` (first error for field)`old(string $field)``mixed` (submitted value, type-transformed)`csrfToken()``string`Controlled zone: `src/Form/`
----------------------------

[](#controlled-zone-srcform)

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
--------------------

[](#boundary-enforcement)

BlockedUse 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
-------

[](#license)

MIT

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance89

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

2

Last Release

54d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/e9233f415f42755f68dc3006e2853b76c34de54b04c9535202b70aa147790e6a?d=identicon)[samaphp](/maintainers/samaphp)

---

Top Contributors

[![samaphp](https://avatars.githubusercontent.com/u/531627?v=4)](https://github.com/samaphp "samaphp (3 commits)")

---

Tags

phpvalidationformscafera

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/scafera-form/health.svg)

```
[![Health](https://phpackages.com/badges/scafera-form/health.svg)](https://phpackages.com/packages/scafera-form)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.5M370](/packages/easycorp-easyadmin-bundle)[kimai/kimai

Kimai - Time Tracking

4.7k8.7k1](/packages/kimai-kimai)[barbieswimcrew/zip-code-validator

Constraint class for international zipcode validation

802.4M](/packages/barbieswimcrew-zip-code-validator)[fp/jsformvalidator-bundle

Javascript validation for sf\[2|3|4\] forms.

127421.3k1](/packages/fp-jsformvalidator-bundle)[chameleon-system/chameleon-base

The Chameleon System core.

1027.9k4](/packages/chameleon-system-chameleon-base)[progsmile/request-validator

Simple PHP Request Validator

33114.0k1](/packages/progsmile-request-validator)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
