NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Forms for Laravel

DI and actions

The facade is the recommended default, not the only way in. There are three equivalent entry points:

  • The Forms facade — shortest, and what the rest of these docs use.
  • The manager, RoundlyConsulting\Forms\FormsManager — the facade root. Inject it through the constructor for the same API with an explicit dependency and no static calls.
  • Actions — single-purpose classes in RoundlyConsulting\Forms\Actions with an execute() method, for composing into your own actions, jobs and commands.

Injecting the manager

use Illuminate\Http\Request;
use RoundlyConsulting\Forms\FormsManager;

final class ContactFormController
{
    public function __construct(private FormsManager $forms) {}

    public function store(Request $request)
    {
        $form = $this->forms->find('contact');

        $this->forms->validate($form, $request);
        $result = $this->forms->submit($form, $request, $request->user());

        $this->forms->submission($result->uuid)->get();   // builders and handles work too

        return back()->with('reference', $result->uuid);
    }
}

FormsManager is an autowired singleton and deliberately not final: Forms::fake() swaps in FormsFake, a FormsManager subtype, so injected managers are faked too.

Calling an action

use RoundlyConsulting\Forms\Actions\FindFormAction;
use RoundlyConsulting\Forms\Actions\StoreSubmissionAction;

// Resolve from the container (or inject into your own action, job or command).
$form = app(FindFormAction::class)->execute('contact');
$result = app(StoreSubmissionAction::class)->execute($form, $request, $user);

The manager resolves each action from the container per call, so binding your own implementation of an action changes the facade’s behaviour as well. The fake records at the manager — an action you call directly is not recorded.

Facade method → action

Facade methodActionexecute()
find()FindFormAction(string $key): Form
create(), define()->create()CreateFormAction(FormDefinitionData $data): Form
update()->save()UpdateFormAction(FormDefinitionData $data): Form
sync(), forms:syncSyncFormsAction(?array $definitions = null): list<string>
validate()ValidateSubmissionAction(Form $form, Request $request): array
submit()StoreSubmissionAction(Form, Request, ?Model $sender, bool $bypassClosed): SubmissionResult
draft()DraftSubmissionAction(Form, Request, ?Model $sender, ?string $uuid, bool $bypassClosed): SubmissionResult
finalize(), submission()->finalize()FinalizeSubmissionAction(string $uuid, bool $bypassClosed = false): SubmissionResult
submission()->model(), review($uuid)FindSubmissionAction(string $uuid): FormSubmission
review()->…->open()ReviewSubmissionAction(FormSubmission, array $approvers, ApprovalRule $rule, ?int $quorum): ApprovalRequest
createSubmission()CreateSubmissionAction(SubmissionData $data): Submission

ValidateFieldTypesAction and CreateFormSubmissionAction are @internal building blocks of validation and submitting — not for host use.

Actions that take DTOs

CreateFormAction and UpdateFormAction take a FormDefinitionData (build it with FormDefinitionData::fromArray()); CreateSubmissionAction takes a SubmissionData (SubmissionData::forField()). The raw row action skips the closed-form check that Forms::createSubmission() adds:

use RoundlyConsulting\Forms\Actions\CreateFormAction;
use RoundlyConsulting\Forms\Actions\CreateSubmissionAction;
use RoundlyConsulting\Forms\DataTransferObjects\FormDefinitionData;
use RoundlyConsulting\Forms\DataTransferObjects\SubmissionData;

$form = app(CreateFormAction::class)->execute(FormDefinitionData::fromArray([
    'key' => 'newsletter',
    'name' => 'Newsletter',
    'groups' => [],
]));

// The raw row write — no closed-form check (Forms::createSubmission() adds it).
$row = app(CreateSubmissionAction::class)->execute(
    SubmissionData::forField($field, $uuid, ['value' => 'Jane'], $user),
);

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.