FOSUserBundle and Bootstrap 3 Template Customisation


In this video we will start customising the various templates that FOSUserBundle provides to us as part of the installation process.

However, we won't be directly editing the templates that live in the downloaded FOSUserBundle directory (/vendor/friendsofsymfony/user-bundle/Resources/views/), but rather using a cool Symfony 'trick' to provide our own templates which will then take precedence over the FOSUserBundle provided ones.

Sound confusing?

Hopefully not, but if it does, the key point is to understand that Symfony will look in two different places for a template.

Let's pretend we have an imaginary Bundle called ExampleBundle, and we want to customise / override the ExampleBundle:SomeDir:example.html.twig template.

Whenever we use that template in our code - for example, as part of rendering in a Controller action - Symfony will look in the following places:

  1. app/Resources/ExampleBundle/views/SomeDir/example.html.twig
  2. vendor/imaginary-vendor-name/ExampleBundle/Resources/views/SomeDir/example.html.twig

The tricky part is that the two paths don't match up entirely (note what follows Resources in both examples), and that is usually the bit I make a mess of when overriding a template structure.

This is important to get right, as if you don't, you won't see an error - just the original template(s).

It's not just Symfony's templates that can be overridden, so if you'd like to know more there is a section in the official Symfony Templating documentation,

There is also another way of overriding templates which is a little more involved, and that is by using Bundle Inheritence. Generally I would advise not to use this unless you are overriding more than just a few templates. If you do go down this route, be sure to check out the FOSUserBundle documentation, and also this more generic Cookbook entry, both of which are worth reading.

Template Tweaking

You don't have to copy all the templates across, but for the purposes of this demonstration I am going to do so.

Any templates that are not copied will revert back to the template from FOSUserBundle.

For me, I find copying all the files across keeps things easier to comprehend for other team members, but your mileage may vary. The downside to copying all templates across is that you need to then diff every template with the Bundle equivalent when updating FOSUserBundle.

To complete this step, I have created a new directory structure:

/app/Resources/FOSUserBundle/views/

Then I have selected all the directories, and the layout.html.twig template from:

/vendor/friendsofsymfony/user-bundle/Resources/views/

And copied / pasted the whole lot under /app/Resources/FOSUserBundle/views/.

You will need to clear your cache (php app/console cache:clear) to see this change, remembering to add in your environment flag (php app/console cache:clear -e=prod) if working in a different environment.

Observation Station

Perhaps another peculiar step here is how we know to use FOSUserBundle inside our path: /app/Resources/FOSUserBundle/views/.

Note that in the vendor path, there is no mention of FOSUserBundle, instead it is called friendsofsymfony/user-bundle.

How did we know what to change this too?

FOSUserBundle is just like any other Symfony Bundle. It has a file (FOSUserBundle.php) that extends Symfony\Component\HttpKernel\Bundle\Bundle. This same convention applies whether we were overriding out own template or a third party bundle. We simply use the bundle name.

Bootstrap / Custom CSS Integration

Likely you are wanting to make your shiny new templates fit in with the look and feel of the rest of your site.

Doing so is remarkably straightforward.

Each of the templates provided by FOSUserBundle extends a base layout. This is very helpful for us, as it means we can switch to Bootstrap, Foundation, or what have you, with not a lot of effort at all:

<!-- /vendor/friendsofsymfony/user-bundle/Resources/views/ChangePassword/changePassword.html.twig -->
{% extends "FOSUserBundle::layout.html.twig" %}

We have taken a direct copy of these templates, so our changePassword.html.twig file looks like this:

<!-- /app/Resources/FOSUserBundle/views/ChangePassword/changePassword.html.twig -->
{% extends "FOSUserBundle::layout.html.twig" %}

Notice, the extends statement hasn't changed.

The template FOSUserBundle::layout.html.twig lives in our revised / overridden structure also:

/app/Resources/FOSUserBundle/views/layout.html.twig

Again, at present this is a like-for-like copy of the original template from FOSUserBundle.

I'm making an assumption here that you would already have a base.html.twig file which you would be extending all your other templates from. In our case, this template file would live at:

/app/Resources/views/base.html.twig

To change the FOSUserBundle templates to start using Bootstrap / Foundation / other, all we need to do is change the single extends statement in the to layout.html.twig file to instead, point to our base.html.twig.

We will also want to remove the <html>, <head> and other tags we already have in our base.html.twig file.

Here's how our templates look now:

<!-- /app/Resources/FOSUserBundle/views/layout.html.twig -->

{% extends '::base.html.twig' %}

{% block content %}

    <div>
        {% if is_granted("IS_AUTHENTICATED_REMEMBERED") %}
            {{ 'layout.logged_in_as'|trans({'%username%': app.user.username}, 'FOSUserBundle') }} |
            <a href="{{ path('fos_user_security_logout') }}">
                {{ 'layout.logout'|trans({}, 'FOSUserBundle') }}
            </a>
        {% else %}
            <a href="{{ path('fos_user_security_login') }}">{{ 'layout.login'|trans({}, 'FOSUserBundle') }}</a>
        {% endif %}
    </div>

    {% if app.request.hasPreviousSession %}
        {% for type, messages in app.session.flashbag.all() %}
            {% for message in messages %}
                <div class="flash-{{ type }}">
                    {{ message }}
                </div>
            {% endfor %}
        {% endfor %}
    {% endif %}

    <div>
        {% block fos_user_content %}
        {% endblock fos_user_content %}
    </div>

{% endblock %}

Note, I have changed the block name from body to content. This is personal preference and works for me, it is not essential / a requirement by any means.

And the base.html.twig file:

<!-- /app/Resources/views/base.html.twig -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="icon" href="favicon.ico">
    {% block stylesheets %}{% endblock %}

    <title>{% block title %}Something Clever Here!{% endblock %}</title>

    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <!-- Latest compiled and minified JavaScript -->

    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
    <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
</head>

<body>

<div class="container">
    {% block content %}{% endblock %}
</div>

<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
{% block javascripts %}{% endblock %}
</body>
</html>

This is simply a combination of the basic Bootstrap template, and some twig specific bits (block content etc) to make the template work with Symfony.

Now, because our base.html.twig file has the Bootstrap style sheets, anything that extends from this template will also get access to the Bootstrap styles.

As such, we can now go back into our various FOSUserBundle forms and start customising and adding in Bootstrap styling.

It is as simple as swapping out the Bootstrap style sheet references in the base.html.twig file with calls to Foundation, or your own custom CSS set up, and your FOSUserBundle forms will all get access to these styles.

You're free to style up the various forms as you see fit. From the video, and not perhaps the most visually exciting thing I have ever produced, but here is an example of the customised /app/Resources/FOSUserBundle/views/Security/login.html.twig template:

{% extends "FOSUserBundle::layout.html.twig" %}

{% trans_default_domain 'FOSUserBundle' %}

{% block fos_user_content %}
{% if error %}
    <div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}

<form action="{{ path("fos_user_security_check") }}" method="post">
    <input type="hidden" name="_csrf_token" value="{{ csrf_token }}" />

    <div class="form-group">
        <label for="username">{{ 'security.login.username'|trans }}</label>
        <input type="text" id="username" name="_username" value="{{ last_username }}" required="required" />
    </div>

    <div class="form-group">
        <label for="password">{{ 'security.login.password'|trans }}</label>
        <input type="password" id="password" name="_password" required="required" />
    </div>

    <div class="checkbox">
        <input type="checkbox" id="remember_me" name="_remember_me" value="on" />
        <label for="remember_me">{{ 'security.login.remember_me'|trans }}</label>
    </div>

    <input type="submit"
           class="btn btn-success"
           id="_submit"
           name="_submit"
           value="{{ 'security.login.submit'|trans }}" />
</form>
{% endblock fos_user_content %}

PHPStorm Template Plugin

In the video I use various PHPStorm plugins to make life easier for myself. One of these is the template helper, which will automagically suggest possible templates based on what I am typing. Well worth using if you are a PHPStorm user.

Code For This Course

Get the code for this course.

Episodes