Symfony bundle that provides a fully-featured contact form with built-in spam protection, reCaptcha v3, rate limiting, event-driven customization, and multilingual support.
- Contact form at
/contact(or/{_locale}/contactfor multilingual apps) - Pre-fills name and email when a user is logged in
- Sends emails via Symfony Mailer (
TemplatedEmail) - Dispatches events to customize form behavior and email content
- Anti-spam: dynamic honeypot with randomized field names and labels per session
- Anti-spam: minimum submission delay check to reject bot submissions
- Anti-spam: reCaptcha v3 via
karser/KarserRecaptcha3Bundle - Rate limiting: optional limits by IP and by email address
- GDPR consent checkbox (configurable)
- Optional "receive a copy" checkbox for the sender
- Subject pre-fill via URL parameter (
?s=My+Subject) - Configuration managed via c975L/ConfigBundle
- PHP >= 8.0
- Symfony >= 7.0
- c975L/ConfigBundle
- c975L/SiteBundle
- karser/karser-recaptcha3-bundle
composer require c975l/contactform-bundleAdd the following to config/routes.yaml:
c975_l_contact_form:
resource: "@c975LContactFormBundle/"
type: attribute
prefix: /
# For multilingual websites:
# prefix: /{_locale}
# defaults: { _locale: '%locale%' }
# requirements:
# _locale: en|fr|esphp bin/console c975l:config:load-allThen use the ConfigBundle dashboard to set the values for each key.
Create your keys on Google reCaptcha and set them in your .env.local:
RECAPTCHA3_KEY=your_site_key
RECAPTCHA3_SECRET=your_secret_keyOr store them via the ConfigBundle dashboard (recaptcha3-site-key and recaptcha3-secret-key).
Create templates/bundles/c975LContactFormBundle/layout.html.twig and extend your own layout:
{% extends 'layout.html.twig' %}
{% set title = 'Contact' %}
{% block content %}
{% block contactform_content %}
{% endblock %}
{% endblock %}The email templates are loaded from SiteBundle. Override them in templates/c975LSiteBundle/emails/.
The route name is contactform_display. Link to it from Twig:
{{ path('contactform_display') }}Pass the s query parameter to pre-fill the subject field (rendered as read-only):
https://example.com/contact?s=My+Subject
If the following Symfony RateLimiter services are defined, they are automatically applied before any email is sent:
limiter.contact_form_by_iplimiter.contact_form_by_email
Example (config/packages/rate_limiter.yaml):
framework:
rate_limiter:
contact_form_by_ip:
policy: sliding_window
limit: 5
interval: '10 minutes'
contact_form_by_email:
policy: sliding_window
limit: 3
interval: '10 minutes'If you have disabled unsafe-inline for style-src in your Content Security Policy, add this rule to keep the honeypot hidden:
.sr-only {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}Two events allow customization without modifying the bundle:
| Constant | Event name | Fired when |
|---|---|---|
ContactFormEvent::CREATE_FORM |
c975l_contactform.create.form |
The form is being built |
ContactFormEvent::SEND_FORM |
c975l_contactform.send.form |
The form has been submitted and validated |
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use c975L\ContactFormBundle\Event\ContactFormEvent;
class ContactFormSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
ContactFormEvent::SEND_FORM => 'onSendForm',
];
}
public function onSendForm(ContactFormEvent $event): void
{
$subject = $event->getFormData()->getSubject();
if (str_contains((string) $subject, 'some-keyword')) {
$event->setEmailData([
'subject' => 'Custom email subject',
'bodyEmail' => 'emails/custom_contact.html.twig',
'bodyData' => [],
]);
// Or abort sending with an error code:
// $event->setError('error.user_not_found');
}
}
}public function onCreateForm(ContactFormEvent $event): void
{
$event->getRequest()->getSession()->set('redirectUrl', 'https://example.com/thank-you');
}If this bundle saves you development time, consider sponsoring via the Sponsor button at the top of the repository. Thank you!