akyos/form-bundle
Configurable contact forms for Symfony (Contact Form 7 style): dynamic fields, submissions, export and Twig rendering. Depends on CoreBundle and CmsBundle. Requires PHP 8.4+ and Symfony 8.
Reality check before you start: this is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/form.php. If your day-to-day is Laravel, you will not drop this into a Laravel app — Laravel's Request, FormRequest, validation, and Blade have no mapping to what this bundle ships. You leverage it when you're working in (or migrating to) a Symfony 8 / PHP 8.4 codebase, or when you need a reference implementation of Contact Form 7-style forms.
Minimal steps
>= 8.4 and Symfony ^8.0. Symfony 8 hard-requires PHP 8.4, so the "PHP 8.2+" note in some docs is stale.composer require akyos/form-bundle:^8.0
You need akyos/core-bundle ^8.0 and akyos/cms-bundle ^8.0 present and enabled first.config/bundles.php (Core, Cms, then Form), in that order.config/packages/mailer.yaml must point at something real before your first test.handleRequest(), redirect on valid submit.Where to look first
ContactMessage::create() + Mail::to() flow.Skip it if you need Laravel-native forms. In Laravel, the equivalent day-to-day job (dynamic fields + stored submissions + export + email + anti-spam) is a migration, a model, a FormRequest, a Mailable, and a Blade partial — roughly a day of work, with full control and no Symfony coupling.
Treat it as a configurable form engine, not a form builder. The value proposition is "define fields, get submissions stored, exported, and rendered." So the pattern that pays off is: define field sets once, reuse them across contexts, and keep your own business logic in listeners rather than in the form type.
Pattern 1 — Wrap the shipped form type instead of editing it.
Subclass or set it as getParent(), then add your fields. This survives bundle upgrades, which matters because the package is at 0 stars and moving fast (patch releases changing request handling is a signal).
class EnquiryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('phone', TextType::class, ['required' => false]);
}
public function getParent(): string
{
return ContactType::class; // bundle type
}
}
Register it as a form.type. Do not copy the vendor file into your app.
Pattern 2 — Controller as a thin adapter.
Laravel devs reach for a FormRequest; here the form type is the validation layer. Keep the controller to three lines of intent: create, handle, redirect. Never fall through to rendering on success — the bundle does not redirect for you, and a refresh will resubmit.
Pattern 3 — Submissions are the integration point.
Everything you'd do in a Laravel store() method — persist, notify Slack, fire a webhook, hit a CRM — belongs in an event listener on the bundle's submit event, not in the controller. This is the single most important habit for a Laravel dev: your controller stops being the orchestrator, and the event becomes it. It also means one listener covers the admin form, the public form, and any programmatic submission.
Pattern 4 — Override by convention, not by forking.
Email templates and translations resolve from the bundle and can be shadowed by putting a same-named file in your app's templates/ or translations/. Copy the vendor file to the matching path in your project and edit there. Same idea for swapping the mailer: bind your own implementation to the bundle's mailer interface in services.yaml. This is the Symfony analogue of binding an interface in a Laravel service provider — reach for it before you patch vendor code.
Pattern 5 — Config-driven spam protection. reCAPTCHA v3 is wired through the Karser bundle. Enable and key it in the bundle config, then make sure the reCAPTCHA script is actually on the page in your base template. Config alone is not enough — a missing script tag yields a silent validation failure that looks like a server bug.
Pattern 6 — Export as an ops workflow. The export feature is for the person who owns the inbox, not the developer. Wire it into an admin route with proper access control from day one; it dumps personal data. If you're in the EU, that route is a GDPR surface, so log who exports what.
Pattern 7 — If you must touch admin builder code, use request bags.
Any custom controller, decorator, or listener you write that reads request input must use $request->query->get(...) / $request->request->get(...). Request::get() was removed in Symfony 8. If you're porting Laravel muscle memory, $request->input('x') becomes $request->request->get('x') for POST and $request->query->get('x') for GET — and you must pick the right bag.
Version floor is a hard gate, and the docs disagree with themselves. README says PHP >= 8.4 / Symfony ^8.0; one assessment says PHP 8.2+. Trust the README and the composer constraint. On Symfony 8 you are on 8.4 regardless, so pin ^8.0.3 explicitly rather than ^8.0.
8.0.3 is not optional if you use admin builder editing. Symfony 8 removed Request::get(), which threw UndefinedMethodError when editing builder components. If admin editing breaks with that error, it's the version, not your code — bump to ^8.0.3. Audit your own code for ->get( on the request too; the bundle fix doesn't fix your controllers.
Zero stars, zero dependents, one maintainer-ish vendor. No community to lean on, and dependency churn is real (the 8.0.3 patch proves it). Mitigations: pin exact versions in composer.json, commit composer.lock, and avoid subclassing or reflecting into internals. Vendor-patch only as a last resort, with a note to revisit.
Three-bundle dependency chain is your first failure mode. Core → Cms → Form, in that order. A missing or version-mismatched cms-bundle produces confusing container errors rather than a clean "missing dependency" message. If the container won't compile, check the chain before you debug your own config.
Silent form failures usually mean reCAPTCHA, mailer, or CSRF — in that order.
@dev stability in some references — treat that as a supply-chain risk and check what you actually resolved in the lock file.form_start()/form_end(). Use the helpers and let them do it.Submissions persist but email fails silently. Design for this: your event listener should be the durable path (store first, then attempt notification), and you should monitor for submissions without a corresponding send. This bundle won't queue or retry for you.
No redirects, no queueing, no metrics. Three things a Laravel dev assumes come for free. Redirect explicitly on success. If volume grows, the synchronous mail send inside the request becomes your latency and timeout source — move notification into a Symfony Messenger handler rather than waiting for the bundle to grow one.
Debugging workflow that actually works
APP_DEBUG=true, then use the profiler at /_profiler: the Forms panel tells you whether validation failed and on which field, and the Events panel tells you whether your submit listener fired. This is the equivalent of Laravel Telescope and it answers most "why didn't it work" questions faster than logging.dump($request->query->all(), $request->request->all()) when a value is unexpectedly null — confirms which bag holds it.dump() inside your submit listener to confirm the submitted payload shape before you write mapping logic against it. The docs are thin enough that the payload shape is empirical, not documented.Laravel-dev translation table, to stop the reflexive wrong turn
| Laravel habit | Here |
|---|---|
FormRequest for validation |
Rules live in the form type's field options |
$request->input('x') |
$request->request->get('x') (POST) / $request->query->get('x') (GET) |
Controller store() does everything |
Event listener does everything |
Mail::to()->send() in controller |
Bundle mailer + overridable template |
config/form.php |
config/packages/*.yaml, container parameters |
| Publish vendor views | Copy same-named file into templates/ |
| Service provider binding | services.yaml interface→implementation |
Extension points to prefer, ranked
The honest verdict for a Laravel developer: treat this as a Symfony-only tool and as a pattern reference for building a CF7-style form system elsewhere. Its architecture — declarative fields, stored submissions, export, event-driven side effects — is a sound blueprint you can reproduce in Laravel with migrations, a FormRequest, a Mailable, and a FormSubmitted event, and you'd get better docs, active maintenance, and no three-bundle chain for the effort.
How can I help you explore Laravel packages today?