Derafu Form

Define a form once, as data. Render it, validate it and reuse it.

A form is data, not HTML

Most form libraries tie the definition of a form to the HTML that draws it. In Derafu Form a form is plain data: a JSON Schema that says what the fields are, and a UI schema, in the style of JSON Forms, that says how they are laid out.

The definition is rendered on the server and validated by Derafu Data Processor when the request comes back. Because it follows JSON Forms, a simple form can also be drawn in the browser by that library, but Derafu Form adds features of its own that JSON Forms cannot draw. The validation always stays on the server.

How it works

The data, the layout, the page and the request: one definition for all of them.

Describe the data

A JSON Schema with the types, the formats and the required fields. It is also the first source of the validation rules.

Describe the layout

A UI schema with layouts, groups, tabs and rules that show or hide fields. No HTML to write.

Render it

One Twig call draws the whole form, or you draw it row by row when you need control.

Process the request

The data goes through cast, transform, sanitize and validate. If something fails, you get the errors and the form, ready to draw again.

See it working

Live forms from the documentation. Open one and use it.

One definition, the whole loop

From the definition to the page, and back from the request.

// A form is data. Three parts:
//   schema    what the data is: types, formats, which fields are required
//   uischema  how it is laid out
//   data      the values it starts with
$definition = [
    'schema' => [
        'type' => 'object',
        'properties' => [
            'name' => ['type' => 'string', 'title' => 'Full Name'],
            'email' => ['type' => 'string', 'format' => 'email', 'title' => 'Email Address'],
        ],
        'required' => ['name', 'email'],
    ],
    'uischema' => [
        'type' => 'VerticalLayout',
        'elements' => [
            ['type' => 'Control', 'scope' => '#/properties/name'],
            ['type' => 'Control', 'scope' => '#/properties/email'],
        ],
    ],
    'data' => ['name' => '', 'email' => ''],
];

$form = $formFactory->create($definition);
{# The whole form, in one call. #}
{{ form(form) }}

{# Or row by row, when you need control over the page. #}
{{ form_start(form) }}
    {{ form_row(form.fields.name) }}
    {{ form_row(form.fields.email) }}
    <button type="submit" class="btn btn-primary">Submit</button>
{{ form_end(form) }}

{# Options such as floating labels are given when rendering, to the form, or to one field. #}
{{ form(form, {floating_labels: true}) }}
// The data of the request goes through four phases, always in this order:
// cast, transform, sanitize and validate.
$result = $processor->process($form, $_POST);

if ($result->hasErrors()) {
    $result->getErrors();         // ['email' => ['...the message...']]
    $result->getForm();           // The form with its errors, to draw it again.
} else {
    $result->getProcessedData();  // The data, cast and clean, ready to use.
}

// And a typed reader over the processed data, for nested values:
$result->getReader()->requireString('contact.address');
// The rules of a field come from three places, joined in this order:
//   1. the schema        ('type', 'format', 'pattern', limits, 'enum')
//   2. the UI schema     (a file control validates a file, for example)
//   3. the 'rules' of the definition, when you need something more:
'rules' => [
    'name' => [
        'transform' => ['uppercase'],
        'validate' => ['min_length:3'],
    ],
],

// The same engine, Derafu Data Processor, also works alone, on any value:
$result = $dataProcessor->process(' [email protected] ', [
    'transform' => ['lowercase'],
    'sanitize' => ['trim'],
    'validate' => ['email', 'max_length:255'],
]);

// Files and arrays too:
$dataProcessor->process($_FILES['upload'], [
    'validate' => ['file:2M', 'mimetype:application/pdf,image/*'],
]);
// A simple form can be drawn in the browser by JSON Forms, with the same definition.
// Derafu Form adds features that JSON Forms cannot draw: a form that uses them
// works on the server only.
// It is optional: the server renders the form without any JavaScript.
import './jsonforms-viewer.js';

const formDefinition = {{ jsonFormsDefinition | json_encode | raw }};
const formInstance = window.renderJsonForm('json-form-container', formDefinition);

// formInstance gives you the data and the errors of the form.
# CSRF: one token for each form. The form draws it and checks it when it is processed.
# It needs the session of the request and one middleware. In a Symfony application,
# the bundle uses the CSRF manager of the application instead.

# Captcha: you choose the provider with environment variables.
CAPTCHA_PROVIDER=altcha       # hcaptcha, turnstile, recaptcha-v3, altcha or none
CAPTCHA_SITE_KEY=...          # the public key (altcha does not use it)
CAPTCHA_SECRET_KEY=...        # the secret key (altcha: any long random text)

# altcha: the browser solves a proof of work. No third party and no account.
# A provider with its keys missing is a configuration error with a message that
# says what to fix. It is never a captcha that fails silently.

Frequently asked questions

What it does, and what it leaves to you.

No. The server renders the form with Twig and it works without any JavaScript. JSON Forms is optional: it can draw the simple forms from the same definition, and then you need its npm packages (React and Material UI). Derafu Form adds features of its own that JSON Forms cannot draw, so not every form works in both.

On the server, when the request comes back. The browser also gets the native required attribute on the required fields, as a help, but it is never what decides.

The UI schema has rules that show or hide a field, and there are selects in a cascade. A field whose rule is not active is not validated and does not appear in the processed data, even if it came with the request.

Yes, even in the same template. Derafu Form is an alternative with its own definitions, not Symfony form types, but both define Twig functions with the same names (form, form_row, form_widget and more), and a Twig environment keeps one function for each name. The Symfony bundle solves it: when a template is compiled, each call to one of those functions becomes a choice made when the page is rendered, by its first argument. A Derafu form or field is drawn by Derafu, and a Symfony FormView is drawn by Symfony, with its own themes. You do not need prefixes. Symfony Form has to be enabled only if you use it.

It connects Derafu Form to the application: the forms are PHP files in resources/forms/ (also inside your bundles), loaded by name; they are rendered in the Twig environment of the application, where your templates in templates/form/ take precedence; the messages are translated by the Symfony translator; the JavaScript widgets are added to the import map of AssetMapper; and, if the application has the CSRF manager of Symfony, the tokens of the forms are generated and checked by it.

The CSRF token is one for each form: the form draws it and checks it when it is processed. Outside Symfony, the manager of Derafu works on the session of the request (Mezzio) and one middleware. In Symfony, the bundle uses the CSRF manager of the application. The captcha is chosen with environment variables: hCaptcha, Turnstile, reCAPTCHA v3 or ALTCHA. If you use ALTCHA, you install its package, because the other providers do not need it.

PHP 8.5 or later, with the intl extension for the format rules of Data Processor. The Derafu packages are published as dev-main for now.

Where it fits

Other good tools exist. Choose by how you work.

Approach Examples Choose it when Derafu Form instead
The form library of a framework Symfony Form, Laminas Form, Nette Forms You want the ecosystem of that framework: its types, extensions and transformers. A form is data, not code, so it needs no framework, and the layout and the validation are part of the definition.
HTML from a JSON Schema kingsquare/json-schema-form, schema-forms (Drupal) You only need a basic form from a schema, or you are in Drupal. Layouts, tabs and conditional fields in the UI schema, with the validation and the errors of the request included.
A JSON Schema from your forms Liform (Symfony) Your forms already exist in Symfony and a JavaScript client needs their schema. It goes the other way: the schema is the source, and the form comes from it.

Install it

The form comes with its validation. CSRF and captcha are added when you need them.

# The Derafu packages are published as dev-main for now, so your composer.json needs:
#   "minimum-stability": "dev", "prefer-stable": true
composer require derafu/form            # includes derafu/data-processor

# Protection, optional:
composer require derafu/csrf derafu/captcha

# In a Symfony application:
composer require derafu/symfony-form-bundle

Next: the Form documentation, the Data Processor documentation and the Symfony bundle documentation explain the rest.

Ready to define a form once?

Open one of the live examples, then write your own definition.