Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Form Bundle Laravel Package

atoolo/form-bundle

Provides an HTTP interface for displaying and processing forms using JSON Forms in Symfony applications.

View on GitHub
Deep Wiki
Context7

Getting Started

The most important thing to internalise before writing a line of code: atoolo/form-bundle is a Symfony bundle, not a Laravel package. It has no service provider, no facade, no config/form-bundle.php, and no Form facade to reach for. What it actually gives you is an HTTP interface that serves JSON Forms definitions (json-schema + uischema) and accepts submissions for processing.

So for a Laravel team, the realistic day-one decision is architectural, not composer require:

  1. Run it as a sidecar service (recommended). Stand the Symfony app up in Docker alongside your Laravel app, on a private network. Laravel talks to it over HTTP.
  2. Embed a Symfony microkernel in Laravel — possible, but you inherit two DI containers, two error handlers and two upgrade paths. Don't.
  3. Ignore the bundle and use @jsonforms/react / @jsonforms/vue directly with your own Laravel endpoints. This is the fallback if you only wanted the JSON Forms idea, not the bundle.

Assuming option 1:

# docker-compose.yml (sidecar)
forms:
  image: your-registry/atoolo-form-service:1.1.0   # pin the exact tag
  environment:
    APP_ENV: prod
    APP_SECRET: ${FORMS_APP_SECRET}
  networks: [internal]
  expose: ["80"]

Then wire it into Laravel:

// config/services.php
'forms' => [
    'base_url' => env('FORMS_BASE_URL', 'http://forms'),
    'token'    => env('FORMS_SERVICE_TOKEN'),
    'timeout'  => env('FORMS_TIMEOUT', 3),
],
// app/Services/Forms/FormClient.php
namespace App\Services\Forms;

use Illuminate\Support\Facades\Http;

class FormClient
{
    public function definition(string $formId, string $locale = 'en'): array
    {
        return Http::baseUrl(config('services.forms.base_url'))
            ->withToken(config('services.forms.token'))
            ->timeout(config('services.forms.timeout'))
            ->acceptJson()
            ->get("/forms/{$formId}", ['locale' => $locale])
            ->throw()
            ->json();
    }

    public function submit(string $formId, array $data): array
    {
        return Http::baseUrl(config('services.forms.base_url'))
            ->withToken(config('services.forms.token'))
            ->timeout(config('services.forms.timeout'))
            ->acceptJson()
            ->post("/forms/{$formId}", ['data' => $data])
            ->json();
    }
}

Where to look first: the official docs at sitepark.github.io/atoolo-docs/develop/bundles/form/, then — because route names and payload shapes are not in the README — run bin/console debug:router inside the sidecar container to enumerate the actual endpoints. Treat that output as your API contract and commit it to the repo.

First use case: pick exactly one simple form (a contact/support form or an internal settings panel), fetch the definition through Laravel, render it with @jsonforms/react, and POST the result back through the same proxy. Do not start with multi-step or conditional forms.

Implementation Patterns

1. Laravel as a thin, caching proxy. Never let the browser talk to the Symfony service directly. Route through Laravel so you can enforce auth, rate limiting, correlation IDs and caching in one place.

// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
    Route::get('/forms/{form}', [FormController::class, 'show']);
    Route::post('/forms/{form}', [FormController::class, 'submit']);
});

2. Cache the definition aggressively. Schemas change on deploy, not per request. Key by form ID + locale + a version marker, and bust on release.

public function definition(string $formId, string $locale): array
{
    $key = "forms:def:{$formId}:{$locale}:" . config('app.release');

    return Cache::remember($key, now()->addHours(12), fn () =>
        $this->client->definition($formId, $locale)
    );
}

If you run Redis, use tags so a form-editor UI can invalidate a single form without nuking everything.

3. Let the Symfony service be the single source of truth for validation. It is tempting to re-derive Laravel rules from the JSON Schema. Don't, beyond cheap UX-level checks. JSON Schema constructs like oneOf, anyOf, $ref, if/then/else, and format have no faithful Laravel Validator equivalent, and the two will drift the moment someone edits a schema. Render the client-side errors from JSON Forms (AJV) and trust the server response for the authoritative verdict.

4. Adapter for the error shape. JSON Forms/AJV error payloads won't match Laravel's {message, errors: {field: []}} convention. Write one adapter, once:

protected function toValidationException(array $payload): ValidationException
{
    // Inspect the actual 422 body from your service and map here.
    // Typical shape: { errors: [ { instancePath: '/email', message: '...' } ] }
    $errors = collect($payload['errors'] ?? [])
        ->groupBy(fn ($e) => ltrim($e['instancePath'] ?? '', '/'))
        ->map(fn ($group) => $group->pluck('message')->all())
        ->all();

    return ValidationException::withMessages($errors);
}

5. Persist the raw submission as JSON, project later. A form_submissions table with form_id, schema_version, payload (json column), status, submitted_by gives you an audit trail and lets you re-project into typed tables as the schema evolves. Store the schema version returned by the definition endpoint — you'll need it to interpret old rows.

6. Keep side effects off the request path. The submit call is a network hop; don't stack emails, PDFs and third-party calls behind it.

$result = $this->forms->submit($formId, $validated);
$submission = FormSubmission::create([...]);

ProcessFormSubmission::dispatch($submission)->afterResponse();

7. Feature-flag / A-B variants. Because forms are data, shipping a variant is a config change, not a deploy. Resolve the form ID per user cohort, cache each definition separately, and log the variant ID alongside submissions so analytics can attribute conversions.

8. Frontend integration. Use @jsonforms/react or @jsonforms/vue with the definition your proxy returns. Custom widgets (date pickers, rich text, file upload) go in as JSON Forms renderers on the JS side — that's the real extension point for UI, and it means the Symfony side stays generic.

9. Test with Http::fake. Your whole test suite should run without the sidecar:

Http::fake([
    'forms/forms/contact*' => Http::response(['schema' => [...], 'uischema' => [...]]),
]);

Add one separate contract test (nightly, against a running sidecar) that asserts the real endpoints still match your assumptions. That's the guard against a service upgrade silently breaking you.

Gotchas and Tips

  • No Laravel integration exists. No service provider, no publishable config, no .env conventions. Everything — auth, CSRF, session, tenancy — is yours to build in the proxy layer. Budget for that; it's not a "drop-in."
  • Pin everything. 0 stars, 0 dependents, a beta release history (1.0.0-beta.2 → 1.1.0), and a last-release date that looks like a typo. Use an exact image digest and copy the JSON schemas into your repo as fixtures. Assume you may have to fork or replace it.
  • Keep an escape hatch. Put an interface in front of the client (FormDefinitionRepository, FormSubmissionGateway) so swapping to native @jsonforms + Laravel validation later touches one binding, not forty call sites.
  • Auth across the boundary. Call the service with a service token/API key, never forward the end user's Laravel session cookie. The Symfony app's security model is not your app's security model; don't try to make them agree on users.
  • Timeouts and retries. ->timeout(3)->retry(2, 150). Form definitions are safe to retry (idempotent GETs, cached anyway). Submissions are not — send an Idempotency-Key derived from a client-generated UUID and dedupe server-side in Laravel, or a retried double-click creates two tickets.
  • Locale and i18n. JSON Forms does i18n on the client. Make sure your proxy passes a locale to the definition endpoint and caches per-locale; otherwise the first visitor's language gets baked into the cache for everyone.
  • Schema/version drift. When someone edits a form, in-flight sessions may post against an old schema and stored rows reference a version that no longer exists. Always persist schema_version and treat old submissions as read-only snapshots.
  • File uploads are the weak spot. Multipart through a PHP proxy through a Symfony app is unpleasant. If forms need attachments, upload directly to your own storage first and submit an opaque reference in the JSON payload.
  • Debugging spans two apps. A validation failure originates in the Symfony validator and surfaces in a Laravel exception. Add a correlation ID header on every outbound call, log it on both sides, and wrap the client so ConnectionException / 5xx produce a friendly "form temporarily unavailable" rather than a 500 to the user.
  • Watch for 422 vs 400 vs 200-with-errors. Confirm empirically what the service returns on invalid input; don't assume HTTP semantics. ->throw() will mask the body, so check $response->status() before throwing when you need the error detail.
  • Config quirks to probe: whether submissions return a redirect, a token, a stored ID, or nothing; whether the service expects application/json or application/merge-patch+json; whether there's a CSRF/cookie requirement that leaks through. Run debug:router and debug:config once and write the answers into your own README — you will forget.
  • Extension points worth knowing: custom JSON Forms renderers (JS, client-side UI control), custom schema keywords/format validators (service-side), and your own Laravel-side FormDefinitionRepository implementation (swappability). Everything else is a fork.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin