Use Bootstrap 3 and 4 form themes in the same Symfony project
Last year I helped upgrade a Symfony project from Bootstrap 3 to Boostrap 4. It had many different sections - the main application, admin area, marketing site, API, etc - which were all powered by the same Symfony codebase.
We wanted to upgrade the project one section at a time, i.e. move the admin area to Boostrap 4 first, deploy it, then move onto the other areas later.
Given a project structure like this:
templates
├── admin
│ ├── base.html.twig
│ ├── manage_users.html.twig
│ └── nuclear_button.html.twig
├── app
│ ├── base.html.twig
│ ├── dashboard.html.twig
│ └── payment_details.html.twig
└── marketing
├── base.html.twig
├── home.html.twig
└── pricing.html.twig
We’d like to upgrade the admin/
templates to use Bootstrap 4 first without affecting anything else.
Most of this is straightforward if each section uses a different set of CSS files.
Simply update admin.scss
to import bootstrap 4 instead of 3 - simple!
Other aspects are harder though.
What about form themes?
Can we tell the admin/
templates to use the bootstrap_4_layout.html.twig
theme, while keeping bootstrap_3_layout.html.twig
for everything else?
Like most things in software, there are some options!
Tired approach - the form_theme twig tag
Assuming the default form theme is for Bootstrap 3:
# config/packages/twig.yaml
twig:
form_themes: ['bootstrap_3_layout.html.twig']
We can override the form theme on a per-template basis with the {% form_theme %}
twig tag:
{# admin/nuclear_button.html.twig #}
{% form_theme nuclear_launch_form 'boostrap_4_layout.html.twig' %}
{{ form_start(nuclear_launch_form) }}
<div class="alert alert-danger">
Warning! This button will launch the rockets!
</div>
{{ form_row(nuclear_launch_form.big_red_button) }}
{{ form_end(nuclear_launch_form) }}
This is the approach advocated for on the Symfony docs, and works pretty well for everyday use.
However, it doesn’t scale so well for this application, which has a lot of templates, not all of them extending from the same base templates, with multiple forms and partials.
We’d have to add {% form_theme %}
to lots of files, then turn around and remove them all when we change the default theme in config/packages/twig.yaml
to bootstrap_4_layout.html.twig
anyway!
Wired approach - an event listener
What we really want is a way to tell twig:
All templates we render in the admin area should use the Bootstrap 4 form theme. For everything else, use Bootstrap 3.
- Can we create multiple
twig.yaml
configuration files? Not really. - What about multiple twig environments? Way too complicated.
What does setting the twig.form_themes
configuration node to ['bootstrap_3_layout.html.twig']
actually do?
Let’s dive into the Symfony source to investigate.
form_themes
is a configuration node defined in the TwigBundle’s Configuration class.- The Twig dependency injection extension uses that value to set the
twig.form.resources
parameter. - That parameter is injected into the TwigRendererEngine, which in turn is injected into the FormRenderer.
- At the bottom of the chain, the FormRenderer is responsible for actually generating the form elements. It will use the injected
TwigRendererEngine
(an implementation ofFormRendererEngineInterface
) to find and render the form block we’re after, using theboostrap_3_layout.html.twig
theme we configured.
Phew! Okay, what about the
{% form_theme %}
twig tag?
That’s actually fairly straightforward.
- The
FormRenderer
has a setTheme() method. - The FormThemeNode calls this method when that part of the template is evaluated.
Does this mean we can call
setTheme()
in our code too?
It certainly does! Let’s create a listener / form extension to do that for admin routes.
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Twig\Environment;
class FormThemeExtension extends AbstractTypeExtension
{
private $twig;
private $enabled = false;
public function __construct(Twig $twig)
{
$this->twig = $twig;
}
public function onKernelRequest(GetResponseEvent $event)
{
$routeName = $event->getRequest()->attributes->get('_route', '');
// Some logic to determine this is an admin route
if (substr($routeName, 0, 6) === 'admin_') {
$this->enabled = true;
}
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (!$this->enabled) {
// Do nothing if not on an admin route
return;
}
if ($form->getParent() instanceof FormInterface) {
// Don't modify child forms, just the root
return;
}
$this->twig->getRuntime(FormRenderer::class)
->setTheme($view, 'form/bootstrap_4_layout_custom.html.twig');
}
}
There are two parts to this class:
onKernelRequest()
detects when we’re on an admin route and sets the$enabled
property to use later. Make sure to give it akernel.event_listener
tag for thekernel.request
event.buildView()
(part ofSymfony\Component\Form\AbstractTypeExtension
) is called every time a form view is created. This is the perfect time to grab theFormRenderer
and tell it to use a different theme for this form.
Conclusion
That’s it! While it might seem complicated, this approach dramatically reduces the amount of manual updates we have to make.
It also makes it way easier to switch another section of the project over to the newer theme:
public function onKernelRequest(GetResponseEvent $event)
{
$routeName = $event->getRequest()->attributes->get('_route', '');
- if (substr($routeName, 0, 6) === 'admin_') {
+ if (substr($routeName, 0, 6) === 'admin_' || substr($routeName, 0, 4) === 'app_') {
$this->enabled = true;
}
}
These kinds of approaches are only possible because of Symfony’s architecture.
I’ve heard people complain about the sheer amount of code and layers of abstraction it has, but it’s this abstraction that makes it suited for almost any web application problem you can think of. Once you’ve mastered it, Symfony can do basically anything.
Need a hand with your project? Send me an email!