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

Laravel Form Bridge Laravel Package

barryvdh/laravel-form-bridge

Laravel Form Bridge provides a compatibility layer for Laravel 4-style Form and HTML helpers in Laravel 5+, letting legacy Blade templates and form code keep working through the Laravel Collective HTML package. Useful when upgrading older Laravel apps without rewriting every form.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is thin glue: one service provider, a small config surface, and a handful of classes that bind Symfony's form.factory into Laravel's container and let you register form types through it. The README is minimal, and that is the first thing to internalise — your real documentation is the Symfony Form docs, not this repo.

Install and wire up.

composer require barryvdh/laravel-form-bridge

Register the bridge's service provider in bootstrap/providers.php (Laravel 11+) or config/app.php. If you need to change defaults, publish the config; otherwise skip it. There are no migrations, queues, or caches involved — forms are constructed per request and are stateless.

Where to look first, in this order:

  1. vendor/barryvdh/laravel-form-bridge/src/ — read LaravelFormServiceProvider.php end to end. It is small, and it is the only place the Laravel↔Symfony seam exists. Everything else you need to know lives in Symfony's docs.
  2. Symfony's Form component documentation — form types, buildForm, data transformers, form events, CollectionType. This is where 90% of your day-to-day knowledge comes from.
  3. Your own first form type (below).

First use case. Pick one real entity form with three to five fields — a "create post" or "edit customer" screen. Create the type:

namespace App\Forms;

use App\Models\Post;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title', TextType::class, ['label' => __('post.title')])
            ->add('body', TextareaType::class, ['label' => __('post.body')]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Post::class,
            'csrf_protection' => false, // Laravel's VerifyCsrfToken already covers this
        ]);
    }
}

Register it with the bridge (subclass the bridge's provider and use its type-registration hook — the parent class is under 150 lines, confirm the exact method name there rather than trusting a blog post), then in a controller:

public function edit(Request $request, Post $post)
{
    $form = app('form.factory')->create(PostType::class, $post);

    $form->handleRequest($this->toSymfonyRequest($request));

    if ($form->isSubmitted() && $form->isValid()) {
        $post->fill($form->getData() ? [...] : [])->save();

        return redirect()->route('posts.index');
    }

    return view('posts.edit', ['form' => $form->createView()]);
}

If that round-trips a real save, you have validated the whole integration. Do that before building anything else.

Implementation Patterns

Form types are your unit of reuse. One AbstractType per entity or per logical form, not per screen. Use configureOptions(['data_class' => Model::class]) so the form binds directly to an Eloquent model and getData() returns a hydrated instance. Reuse the same type across create and edit — that is the payoff that justifies the abstraction.

Controller shape is always the same: create → handleRequest → isSubmitted() && isValid() → persist → redirect (POST/Redirect/GET). Never render on success. This is the Symfony idiom and it maps cleanly onto Laravel's flash/session conventions.

Choose one validation strategy and commit. Two viable paths:

  • Symfony constraints on the type ('constraints' => [new NotBlank(), new Length(['max' => 255])]) — single source of truth, errors land on the form via $form->getErrors(). Use symfony/validator.
  • Laravel FormRequest on the controller, feeding already-validated arrays into the form as data. Keeps validation in Laravel where your team already lives and where $errors binding works natively.

Mixing both is the most common way this becomes unmaintainable — you get two error bags for one field and no clear owner.

Relations need data transformers. Doctrine's EntityType does not exist here. For an Eloquent relation, use ChoiceType with a choice_loader that queries your model, or write a DataTransformerInterface that maps Model ↔ id. For belongsTo, the transformer is ~20 lines and is the single most-reused piece of code you will write.

Collections for nested rows. CollectionType with allow_add, allow_delete, prototype => true handles line items and address lists well. You must supply the JavaScript to clone the prototype — Symfony's docs give you the recipe; you are just re-implementing the markup in Blade.

Blade rendering is yours. There are no form_row / form_widget helpers shipped here, and Symfony's Twig themes do not port. Establish a convention early:

  • resources/views/forms/field.blade.php — a generic partial taking label, widget, errors, attr.
  • resources/views/forms/{type}.blade.php — per-type layout for anything non-trivial.
  • Render widgets with {!! $view['title']->vars['attr'] ... !!} or a small Blade macro. Build the widget HTML from $view->vars (value, attr, errors) rather than {{ $form.title }}.

Test at the type level. form.factory is stateless and injectable, so you can unit-test a form type in isolation: create the form with known data, submit() an array, assert isValid() and getErrors(). No HTTP, no database, no browser. This is a genuine advantage over testing controller-resident validation.

Gotchas and Tips

CSRF double-enforcement. Symfony Form adds its own _token and CSRF manager by default. Laravel's VerifyCsrfToken middleware also injects and checks _token. Set 'csrf_protection' => false in configureOptions() and let Laravel own CSRF, or you will get intermittent 419s on forms that render fine but post a Symfony-flavoured token.

The Request impedance mismatch. $form->handleRequest() wants Symfony\Component\HttpFoundation\Request. The bridge bridges this, but verify it — if it doesn't, wrap with Request::createFromGlobals() or a small adapter in a base controller. Debugging a form that "won't submit" is almost always this.

Old input after a failed submit. Symfony repopulates from the submitted form object held in memory, not from old(). That works fine on the same-request render. If you redirect-with-errors anywhere, you must serialize the view — $request->session()->put('form', $form->createView()) then createView($stored) — because the form object does not survive the request. Blade {{ old('title') }} will be empty on a Symfony-rendered field.

Datetime and Carbon. DateTimeType yields DateTimeImmutable/DateTime, not Carbon. Eloquent date casts will handle strings and Carbon happily but not always DateTimeImmutable on the way in — normalize in a PRE_SUBMIT or MODEL_TRANSFORMER listener, or cast explicitly before save().

Translations don't route through Laravel. Symfony labels go through its own translator. Set 'translation_domain' => false and wrap labels in __() inside buildForm — otherwise your labels silently render as raw keys.

Block prefix collisions. When two forms share a page, give each a unique getBlockPrefix(). Duplicate IDs in rendered HTML is the symptom.

Debugging errors. $form->getErrorsAsString() was removed; use $form->getErrors(true, true) and iterate FormErrorIterator. In Tinker: app('form.factory')->create(PostType::class) — if that resolves, the bridge is wired; if not, the provider isn't registered or a type is missing from your registration hook.

Pin and read the parent class before upgrading. At 0.7.x there is no SemVer guarantee, and v0.7.5 added getSubscribedEvents(): array — a no-op for consumers, a fatal for anyone subclassing the subscriber without the return type. Pin ~0.7.5 rather than ^0.7, and grep -r getSubscribedEvents app/ before any bump. Keep customisation at the edges (types, transformers, Blade partials) rather than overriding provider internals, so a fork or swap stays a day of work.

No EntityType means you own the choice loader. Cache the query result per request if you render the same select twice; the loader fires per form creation.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console