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

Bus Form Bundle Laravel Package

pugx/bus-form-bundle

PugxBusFormBundle integrates Symfony Forms with a command/message bus, letting form submissions be dispatched as commands and handled by dedicated bus handlers. It keeps controllers thin, separates form handling from business logic, and enables a cleaner, more testable workflow for Symfony applicati...

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register

composer require pugx/bus-form-bundle:^1.5

v1.5.0 raises the floor to PHP 8.3 (8.2 support was dropped in PR #21). Check php -v and your CI matrix before bumping — a composer update on an 8.2 image will either fail resolution or silently keep you on 1.4.x. Pin the exact version while you evaluate.

Symfony Flex usually registers it; if not, add it to config/bundles.php (Symfony 5.4+/6+) and check whether a recipe dropped a config/packages/pugx_bus_form.yaml. If no recipe exists, the bundle likely works with zero configuration and a default bus.

v1.5.0 removed SimpleBus support (PR #22). The only bus abstraction wired up now is Symfony Messenger's MessageBusInterface. If your pugx_bus_form.yaml names a SimpleBus service as the bus, the container will fail to compile — see the Gotchas section for the migration shape.

Where to look first — in this order

  1. README.md — updated wording/badges in v1.5.0, but for a 3-star bundle this is still the real documentation; the API surface is small.
  2. DependencyInjection/Configuration.php — this is the ground truth for every option (bus name, auto-dispatch flag, form type base class). Re-read this after the 1.5.0 upgrade — dropping SimpleBus almost certainly removed a config key, and debug:config will reject a stale one. Ignore the docs if they disagree.
  3. Form/ and EventSubscriber/ (or EventListener/) — find the class that listens on FormEvents::POST_SUBMIT; that's where the MessageBusInterface::dispatch() happens and where the whole package's behaviour lives.
  4. tests/ — the functional tests show the intended controller shape faster than any prose. v1.5.0 moved the suite to phpUnit 13 (PR #16), so if you copy test scaffolding from the bundle, match that major version or you'll fight attribute/assertion deprecations in your own suite.

Confirm what you actually have installed before writing code:

composer show pugx/bus-form-bundle      # confirm 1.5.x, and PHP requirement
php -v                                  # must be >= 8.3
bin/console debug:config pugx_bus_form  # options actually in the tree (changed in 1.5.0)
bin/console debug:container --tag=form.type   # find the bundle's form types

Your first use case: a contact/registration form that dispatches a message instead of writing to the DB in the controller.

// src/Message/RegisterUser.php
final class RegisterUser
{
    #[Assert\NotBlank]
    #[Assert\Email]
    public string $email = '';

    #[Assert\Length(min: 8)]
    public string $password = '';
}
// src/Controller/RegistrationController.php
#[Route('/register', methods: ['GET', 'POST'])]
public function register(Request $request): Response
{
    $message = new RegisterUser();
    $form = $this->createForm(MessageFormType::class, $message); // base type name varies
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        // the bundle has already dispatched on POST_SUBMIT — do NOT dispatch here
        return $this->redirectToRoute('registration_success');
    }

    return $this->render('registration/register.html.twig', ['form' => $form]);
}

Start on the sync transport so the handler runs inline and you can dump() inside it; switch the message's routing to async only once the handler is correct.

Implementation Patterns

Validation lives on the message, not the form. The whole point of the bundle is that the message is the single input contract. Put constraints on the DTO so the same object is valid whether it arrives from HTTP, a console command, or a replay from the failure transport. Keep buildForm() to widget choice, labels, and layout only.

Immutable messages via empty_data. If your message has a constructor (good practice), the default data mapper can't instantiate it. Supply a factory:

public function configureOptions(OptionsResolver $resolver): void
{
    $resolver->setDefaults([
        'data_class' => RegisterUser::class,
        'empty_data' => static fn (FormInterface $form): RegisterUser => new RegisterUser(
            (string) $form->get('email')->getData(),
            (string) $form->get('password')->getData(),
        ),
    ]);
}

Keep the controller to three lines. With auto-dispatch, the controller's only job is createForm → handleRequest → render/redirect. Extract the redirect target into a route constant so the "did it dispatch?" logic never leaks back in.

Command-bus vs. event-bus routing. Register the form-submitted message on your command bus (transactional, one handler, may return a result) rather than the event bus, otherwise you cannot tell the user "handler failed" unless you are on sync. With sync + Doctrine's messenger.middleware.doctrine_transaction you get an all-or-nothing submit for free.

One bus abstraction only. After v1.5.0 there is exactly one supported bus integration: MessageBusInterface from symfony/messenger. That's actually a simplification — routing, middleware, stamps, retries, and the failure transport are all standard Messenger features, so you can lean on the framework docs rather than bundle-specific config. If you need something SimpleBus-shaped, write a thin adapter service implementing MessageBusInterface that forwards to your own bus.

Progressive enhancement / multi-step. Each wizard step is its own short message; hold the accumulated state in the session (or a persisted draft entity with an ID) and dispatch the final "commit" message only on the last step. Never carry an entity inside a message body — carry the ID.

API/JSON endpoints. If you reuse the same form type behind a JSON controller, guard the automatic dispatch: the subscriber fires on POST_SUBMIT for any valid submit, and handleRequest will not mark a JSON body as submitted in older Symfony versions. Either build a separate FormType without the bundle base class, or submit manually with $form->submit($payload, false) and check isValid() before letting the subscriber see it.

Testing. Use the in-memory transport so you can assert on dispatched envelopes without a worker:

$client->submitForm('Register', ['register_user[email]' => '[email protected]', /* ... */]);
$transport = self::getContainer()->get('messenger.transport.async');
self::assertCount(1, $transport->getSent());

For sync messages, assert on the handler side effect (the user row exists) instead — it proves the whole chain. Note the bundle's own suite is on phpUnit 13 as of 1.5.0; if you extend or vendor its tests, keep your phpunit constraint aligned.

Gotchas and Tips

  • Upgrade checklist for 1.5.0. Three things to verify before bumping: (1) PHP >= 8.3 everywhere including CI and Docker; (2) no simple_bus/SimpleBus service reference in config/packages/pugx_bus_form.yaml — delete it or switch the value to your Messenger bus ID; (3) bin/console debug:config pugx_bus_form compiles cleanly, which catches any other key the release removed. Then run your form functional tests on a 1.5.0 branch before merging.
  • Zero dependents, 3 stars. Pin the exact version, and read CHANGELOG/git log before bumping — 1.5.0 is a good example of a "minor" version that contains two breaking-ish changes (PHP floor, SimpleBus removal). Assume you may need to fork; the package is small enough that vendoring the subscriber into your app is a viable escape hatch.
  • Double dispatch is the classic bug. The subscriber fires on POST_SUBMIT; if you also call $bus->dispatch($message) in the controller "just to be safe," every submit is handled twice. Grep your controllers for dispatch( after adding the bundle.
  • isSubmitted() && isValid() in the controller is still required for the redirect. Rendering on a valid-but-already-dispatched form gives the user a re-submittable page backed by a second handler run.
  • Async erases the failure. Once the message goes to a transport, exceptions land in messenger:failed:*, not in the user's face. Configure failure_transport, monitor with bin/console messenger:failed:show, and keep a sync/handleSync variant for user-critical paths (registration, payments).
  • Messages must be serializable. No entities, closures, resources, or lazy proxies for async. Pass scalars/IDs and reload in the handler — this also sidesteps EntityManager-closed errors after a retry.
  • Collections and nested forms arrive as arrays of arrays; type the message property as array and normalise into value objects inside the handler, or the form's ArrayMapper will fight your DTO.
  • Listener ordering. If you have your own POST_SUBMIT subscriber (file upload, audit log), check its priority against the bundle's. If yours must run first, give it a higher priority in getSubscribedEvents(); otherwise the message is already on the bus by the time you mutate the data.
  • CSRF and messenger. CSRF tokens are per-request; nothing about them survives into the async worker. Validate everything security-relevant before dispatch (constraints on the message) rather than relying on anything in the session inside the handler.
  • Debugging checklist: bin/console debug:event-dispatcher FormEvents::POST_SUBMIT to confirm the subscriber is registered, debug:config pugx_bus_form to confirm the bus name (especially after the SimpleBus removal), and messenger:consume -vv to see handler exceptions that the web request swallowed.
  • Extension points: prefer (a) decorating MessageBusInterface for cross-cutting concerns, (b) overriding your message's FormType::buildForm() for UI, and (c) an EnvelopeStamp (tenant ID, correlation ID, user ID) added by a middleware — over patching the bundle. Stamps are the idiomatic way to carry request context into an async handler, and they're now the only bus integration surface, so they're worth learning properly.
  • Laravel mental model: this is FormRequest → dispatch(new Job()) collapsed into one step. The pitfalls you already know transfer directly — validation location, sync-vs-queued, and idempotency on retry — and the package only removes the mapping boilerplate, not the need to think about any of them. After 1.5.0, the bus side is a plain PSR-friendly MessageBusInterface, which maps closely to Laravel's Bus::dispatch()/queue dispatcher, so a custom adapter between the two is trivial if you're bridging systems.
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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums