Getting started

Your first form

Build a form in PHP, show it on an Inertia page, and save it with Eloquent.

This page builds a form that edits an order: one PHP class, one controller and one small Vue page.

Define the form

A form is a class with a schema() method.

namespace App\Forms;

use App\Models\Order;
use Xelqun\Forms\Components\Select;
use Xelqun\Forms\Components\TextInput;
use Xelqun\Forms\Form;
use Xelqun\Forms\Layouts\Section;

class OrderForm extends Form
{
    public function model(): string
    {
        return Order::class;
    }

    public function schema(): array
    {
        return [
            Section::make('Order')->columns(2)->schema([
                TextInput::make('customer_name')->required()->maxLength(255),
                Select::make('country_id')->relationship('country', 'name')->searchable(),
            ]),
        ];
    }
}

Show it and save it

Build the form in your controller. make() fills it from the record, and toSchema() turns it into data for the page.

use App\Forms\OrderForm;
use App\Models\Order;
use Illuminate\Http\Request;
use Inertia\Inertia;

class OrderController
{
    public function edit(Order $order)
    {
        return Inertia::render('Orders/Edit', [
            'form' => OrderForm::make($order)->toSchema(),
            'orderId' => $order->id,
        ]);
    }

    public function update(Request $request, Order $order)
    {
        OrderForm::make($order)->save($request);

        return back();
    }
}

save() validates the request, then creates or updates the record and its relationships inside one transaction.

Render it

Pass the schema to XqForm, and tell it where to submit.

<script setup lang="ts">
import { XqForm, type FormSchema } from '@xelqun/forms';

defineProps<{ form: FormSchema; orderId: number }>();
</script>

<template>
    <XqForm :form="form" :action="`/orders/${orderId}`" method="put" />
</template>

That's a working form, with validation errors shown under each field.

Validate without saving

Not every form writes to a model. validate() returns the cleaned data and leaves saving to you. Hidden and disabled fields are left out of what it returns.

$data = ContactForm::make()->validate($request);

Mail::to('team@example.com')->send(new ContactMessage($data));

Decide who may use it

authorize() runs on every request, including search, uploads and live updates. It allows everything until you say otherwise.

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;

public function authorize(?Authenticatable $user, ?Model $record): bool
{
    return $user !== null && $user->can('update', $record);
}

Hook into saving

Form has three hooks around save():

Method When it runs
beforeSave($data) Inside the transaction, before anything is written. Throwing here stops the save.
afterSave($record, $data) Inside the transaction, after the record and its relationships are saved.
afterCommit($record, $data) Once the outermost transaction has committed. Use it for emails and jobs.

mutateBeforeSave($attributes) changes the attributes just before they reach the model.

View-only forms

disabled() shows a form that nobody can change, as on a View page. The lock travels in the token, so a tampered request is refused on the server too.

OrderForm::make($order)->disabled()->toSchema();