Skip to content

Laravel Integration

Use FormDSL forms in your Laravel app with server-side validation in PHP. Your frontend uses the JSX DSL, your backend validates against the same rules using the exported manifest.

How it works

React/SolidJS form (JSX)
        |
        v
  formdsl manifest          <-- CLI extracts validation rules
        |
        v
  contact.manifest.json     <-- portable JSON contract
        |
        v
  FormDSL\Validate           <-- PHP package validates against it
  1. Author your form in JSX using @formdsl/react
  2. Export the manifest (a JSON file describing all fields and validation rules)
  3. Validate submissions in PHP using the formdsl/validate Composer package

This means your Laravel backend validates using the exact same rules as the client, without running Node.js or a sidecar service.

Step 1: Export the manifest

From your frontend project:

bash
# Single form
npx formdsl-react manifest ./src/forms/ContactForm.tsx -o resources/manifests/contact.json

# All forms in a directory
npx formdsl-react manifest ./src/forms/ -o resources/manifests/

The manifest is a JSON file containing all field definitions, types, validation constraints, and conditions. Commit it to your repo or generate it in CI.

Example manifest (simplified):

json
{
  "formId": "contact",
  "version": "1.0",
  "fields": [
    {
      "id": "name",
      "type": "text",
      "required": true,
      "label": "Full Name",
      "minLength": 2,
      "maxLength": 100
    },
    {
      "id": "email",
      "type": "email",
      "required": true,
      "label": "Email"
    },
    {
      "id": "role",
      "type": "select",
      "required": true,
      "options": [
        { "value": "dev", "label": "Developer" },
        { "value": "design", "label": "Designer" }
      ]
    },
    {
      "id": "github",
      "type": "url",
      "label": "GitHub Profile",
      "showIf": { "role": "dev" }
    }
  ]
}

Step 2: Install the PHP validator

bash
composer require formdsl/validate

Requirements: PHP 8.1+, no external dependencies.

Step 3: Validate in your controller

php
<?php

namespace App\Http\Controllers;

use FormDSL\Validate\Validator;
use Illuminate\Http\Request;

class ContactController extends Controller
{
    public function store(Request $request)
    {
        $manifest = json_decode(
            file_get_contents(resource_path('manifests/contact.json')),
            true
        );

        $result = Validator::validate($manifest, $request->all());

        if (!$result->valid) {
            return back()->withErrors($result->toMessageBag())->withInput();
        }

        // Validation passed — process the submission
        Contact::create($request->only(['name', 'email', 'role', 'github']));

        return redirect()->route('contact.thanks');
    }
}

$result->toMessageBag() returns errors in Laravel's native format:

php
[
    'name' => ['This field is required'],
    'email' => ['Please enter a valid email address'],
]

This works with Blade's @error directive, Inertia's shared errors, and any other Laravel validation error display.

Step 4: Wire up the frontend

Your React form submits to your Laravel endpoint:

tsx
import { Form, Section, Question } from '@formdsl/react'
import '@formdsl/react/styles'

export default function ContactForm() {
  const handleSubmit = async (answers: Record<string, unknown>) => {
    const res = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
      body: JSON.stringify(answers),
    })

    if (!res.ok) {
      const { errors } = await res.json()
      // Handle validation errors from Laravel
      return
    }

    window.location.href = '/contact/thanks'
  }

  return (
    <Form id="contact" version="1.0" onSubmit={handleSubmit}>
      <Section title="Contact">
        <Question id="name" type="text" required label="Full Name" />
        <Question id="email" type="email" required label="Email" />
        <Question id="role" type="select" required label="Role" options={[
          { value: 'dev', label: 'Developer' },
          { value: 'design', label: 'Designer' },
        ]} />
        <Question id="github" type="url" label="GitHub Profile"
          showIf={{ role: 'dev' }} />
      </Section>
    </Form>
  )
}

The client validates on submit (instant feedback), then your Laravel backend validates again server-side (security).

What the validator checks

  • Conditional visibility: Fields hidden by showIf or disabled by disabledIf are skipped
  • Required fields: Respects required and requiredIf conditions
  • Type-specific rules: Format, length, range, pattern, options lists, file types
  • Repeat groups: Recursively validates children within repeat arrays
  • Cross-field matching: match constraints (e.g., confirm email)

API

Validator::validate(array $manifest, array $answers): ValidationResult

Validate answers against a parsed manifest array.

Validator::validateFromFile(string $path, array $answers): ValidationResult

Load manifest from a JSON file and validate.

ValidationResult

  • $result->validbool, whether validation passed
  • $result->errors — array of ValidationError objects
  • $result->toMessageBag()array<string, string[]>, Laravel-compatible error format

ValidationError

  • $error->fieldId — the field that failed
  • $error->message — human-readable error message

Tips

Keep manifests in sync. Re-export after changing your form JSX. Add the manifest export to your CI/CD pipeline:

bash
# In your build script or CI
npx formdsl-react manifest ./src/forms/ -o resources/manifests/

Store manifests in resources/manifests/. Laravel's resource_path() makes them easy to load.

API routes with Inertia. If you're using Inertia, validation errors are automatically shared with the frontend via $page.props.errors.

What's next