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:
app/Resources/ExampleBundle/views/SomeDir/example.html.twig
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.