Symfony2 tutorial: Twig extensions and dependency injection

March 13th 2013

This tutorial is the concluding part of a mini series on Symfony2, specifically: Dependency injection, services and twig extensions. In the previous tutorial we created a service that allows developers to add, check and get flash notification messages. This part of the tutorial will cover writing a twig extension to print out notifications, some js and css being included with assetic, and setting up default bundle configuration with the config class and dependency injection. If you have not followed part 1 of the Symfony2 tutorial, you can find it here: http://www.lrotherfield.com/blog/symfony2-tutorial-creating-and-using-a-service

Twig extension

Rather than get the notifications in the controller and pass them through to every template to render them out, we want to offer developers a simple set of twig functions to do it all for them. To do this we need to create a twig extension. Make a new folder in you bundle called “Twig”. Inside this folder create a new PHP file called “NotificationExtension”:

<?php

namespace LRotherfield\Bundle\NotificationBundle\Twig;

class NotificationExtension extends \Twig_Extension
{

    public function getName()
    {
        return 'lrotherfield_notification_extension';
    }

}

As you can see above, we have extended the Twig_Extension class that implements the Twig_ExtensionInterface for us. You can implement the Twig_ExtensionInterface instead but there is no point as you would just have to create the same methods implemented in Twig_Extension. Now that we have a class, we need to load it as a service and tag it as a twig extension. This is done in the services.yml:

services:
#...
  lrotherfield.twig.message_extension:
    class: LRotherfield\Bundle\NotificationBundle\Twig\NotificationExtension
    arguments: [@service_container]
    tags:
      - { name: twig.extension }

There are a couple of things to note from above:

One

We have passed through one argument above, the entire service_container. It is actually better practice, when you can, to pass through only the services you need which for us will be templating and lrotherfield.notify and look like this:

arguments: [@templating, @lrotherfield.notify]

Because we have set lrotherfield.notify up as a service, we can now pass it through to other services. Unfortunately doing this is not possible with the templating service as it creates a ServiceCircularReferenceException and has been suggested by Fabpot himself that we just inject the whole container.

Two

The second thing to note is the tags argument, we have tagged our service as a twig.extension. Tags are a way of grouping services so that other classes can request all services by a specific tag.

Create the render methods

Our twig extension is now getting loaded so we can create a couple of render methods to be called from any twig template. Initially we need to deal with the service container that is passed through to our __construct() method. We want two services, templating and lrotherfield.notify so we will set them up as individual class variables (due to the templating circular reference issue, we cannot get the templating service in the construct method):

//NotificationExtension.php
//...
use Symfony\Component\DependencyInjection\ContainerInterface;

class NotificationExtension extends \Twig_Extension
{
    protected $container, $notify;

    public function __construct(ContainerInterface $container = null)
    {
        $this->container = $container;
        $this->notify = $container->get("lrotherfield.notify");
    }

Notice above that we are using type hinting to ensure an object that implements ContainerInterface is passed through. To do this we have added a use statement at the top of our class file under the namespace. Now we have our two services we can create our two render functions, one to render specific notifications and one to render all notifications:

//NotificationExtension.php
//...

    public function renderAll($container = false)
    {
        $notifications_array = $this->notify->all();

        if (count($notifications_array) > 0) {
            return $this->container->get('templating')
                ->render(
                    "LRotherfieldNotificationBundle:Notification:multiple.html.twig",
                    compact("notifications_array", "container")
                );
        }

        return null;
    }

    public function renderOne($name, $container = false)
    {
        if (!$this->notify->has($name)) {
            return false;
        }
        $notifications = $this->notify->get($name);

        if (count($notifications) > 0) {
            return $this->container->get('templating')
                ->render(
                    "LRotherfieldNotificationBundle:Notification:single.html.twig",
                    compact("notifications", "container")
                );
        }

        return null;
    }

Currently we are using the templating service to render two view twig files, we will create them later in the tutorial. $container (referenced right at the bottom of this tutorial) will be used in the templates when we get to making them and has no relation to the service container. If you have not come across the compact function before, it creates a key value array using strings to find variables set in your code.

Register the twig functions

Before we can call functions from our class in a twig template we need to register them as twig functions. Twig extensions can register two types of methods, filters and functions. In our case we want to register our methods as functions as we are not looking to manipulate a string like a filter does but render additional view files instead. The method we need to implement is getFunctions() and it needs to return an array of \Twig_function_Method instances:

//NotificationExtension.php
//...
    public function getFunctions()
    {
        return array(
            'notify_all' => new \Twig_Function_Method($this, 'renderAll', array('is_safe' => array('html'))),
            'notify_one' => new \Twig_Function_Method($this, 'renderOne', array('is_safe' => array('html')))
        );
    }

Above we have registered two twig functions:

{{ notify_all() }} will call NotificationExtension->renderAll()
{{ notify_one(name) }} will call NotificationExtension->renderOne($name)

We have also added an array argument array('is_safe' => array('html'))  which tells twig that the returned value of the function is safe and does not need to be cleaned/escaped.

Include the javaScript and CSS files

So that we have nice messages flash notifications rendered to the user, we need a simple js notification library. humane.js suits just fine especially as we don’t need to include jQuery to use it. The simplest method to include humane.js is to create a new twig function that will render both files as script and link tags, this method can then be called in the head of the document. First we need to create a few new directories, Resources/public, Resources/public/css and Resources/public/js. Inside the js and css directories we can then add the corresponding resources. You can get a copy of the minified or regular humane.js here https://github.com/wavded/humane-js, save it as Resources/public/js/humane.js. I have stripped down the css so that it has no colours etc and you can use the stripped down version or use a version from the humane.js git repository:

/* Resources/public/css/humane.css */
.humane{position:relative;-moz-transition:all .6s ease-in-out;-webkit-transition:all .6s ease-in-out;-ms-transition:all .6s ease-in-out;-o-transition:all .6s ease-in-out;transition:all .6s ease-in-out;z-index:100000;filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);}
.humane{font-family:Helvetica Neue, Helvetica, sans-serif;font-size:18px;opacity:0;color:#333;padding:10px;text-align:center;-moz-transform:translateY(-100px);-webkit-transform:translateY(-100px);-ms-transform:translateY(-100px);-o-transform:translateY(-100px);transform:translateY(-100px);}
.humane p,.humane ul{margin:0;padding:0;}
.humane ul{list-style:none;}
.humane-animate{opacity:1;-moz-transform:translateY(0);-webkit-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0);}
.humane-animate:hover{opacity:0.7;}
.humane-js-animate{opacity:1;-moz-transform:translateY(0);-webkit-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0);}
.humane-js-animate:hover{opacity:0.7;filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=70);}

Now that we have the resources, we need a twig function and a view file to include them:

Twig function

public function renderResources()
{
    return $this->container->get('templating')
        ->render("LRotherfieldNotificationBundle:Notification:resources.html.twig");
}

Remember we need to register this function with twig so add another line to the getFunctions() returned array:

'notify_resources' => new \Twig_Function_Method($this, 'renderResources', array('is_safe' => array('html')))

Twig template

In the above function we are trying to render "LRotherfieldNotificationBundle:Notification:resources.html.twig". To make this view we need a new directory Resources/views/Notification. In this directory we can create our view file and include our resource files:

{# Resource/views/Notification/resources.html.twig #}
{% stylesheets '@LRotherfieldNotificationBundle/Resources/public/css/*' %}
    <link rel="stylesheet" type="text/css" href="{{ asset_url }}" />
{% endstylesheets %}

{% javascripts '@LRotherfieldNotificationBundle/Resources/public/js/*' %}
    <script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}

Here we are using assetic to load all of the files in our js and css directories and render them as one file of each type. To use assetic like this we actually need to add a line into our config.yml file:

#config.yml
#...
assetic:
  #...
  bundles: [LRotherfieldNotificationBundle]

Multiple and single notification twig templates

As mentioned earlier, we need two view files to render the output of notify_all() and notify_one(). These views need to loop through the array of notifications passed to them and create humane.js notifications for each one. The files need to be created in Resources/views/Notification:

{# multiple.html.twig #}
<script type="text/javascript">
    var notify = humane.create({container: {{ (container ? "document.getElementById('"~container~"')" : "false") | raw }}});
    {% for notifications in notifications_array %}
        {% for notification in notifications %}
                notify.log("{{ (notification.title == "" ? "" : "<h2>"~notification.title~"</h2>") | raw }}{{ (notification.message == "" ? "" : "<p>"~notification.message~"</p>") | raw }}", { timeout: {{ notification.lifetime }}, clickToClose: {{ notification.click_to_close ? "true" : "false" }}, addnCls: "{{ notification.class }}"});
        {% endfor %}
    {% endfor %}
</script>
{# single.html.twig #}
<script type="text/javascript">
    var notify = humane.create({container: {{ (container ? "document.getElementById('"~container~"')" : "false") | raw }}});
    {% for notification in notifications %}
        notify.log("{{ (notification.title == "" ? "" : "<h2>"~notification.title~"</h2>") | raw }}{{ (notification.message == "" ? "" : "<p>"~notification.message~"</p>") | raw }}", { timeout: {{ notification.lifetime }}, clickToClose: {{ notification.click_to_close ? "true" : "false" }}, addnCls: "{{ notification.class }}"});
    {% endfor %}
</script>

Dependency injection configuration for some defaults

You probably noticed above that we used lots of keys (like notifiaction.click_to_close etc) that currently do not exist. We could require the developer to pass all of them each time they add a notification in the arguments array. This would work, but its not a nice solution especially as quite often many of the values will not change. The other solution is to create some default values and inject them into our service. We can do this using the DependencyInjection/Configuration class by adding nodes to the treebuilder instance that was created when we generated our bundle:

//Configuration.php
//...
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('l_rotherfield_notification');

        $rootNode->children()
            ->scalarNode("message")->defaultValue("")->end()
            ->scalarNode("title")->defaultValue("")->end()
            ->scalarNode("class")->defaultValue("notice")->end()
            ->scalarNode("type")->defaultValue("flash")->end()
            ->scalarNode("lifetime")->defaultValue(6000)->end()
            ->booleanNode("click_to_close")->defaultFalse()->end()
            ->end();

        return $treeBuilder;
    }

Above we have added lots of scalarNodes with default values and one booleanNode. These values can be overwritten by developers in their config.yml files like so:

#config.yml
#...
l_rotherfield_notification:
  click_to_close: true
  lifetime: 5000 #add any of the nodes needed, not just these two

We can now use these values in DependencyInjection/LRotherfieldNotificationExtension to set them as parameters which can then be used in our services.yml file to inject them into our lrotherfield.notify service:

//LRotherfieldNotificationExtension
//...
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $container->setParameter("lrotherfield.notify.message", $config["message"]);
        $container->setParameter("lrotherfield.notify.title", $config["title"]);
        $container->setParameter("lrotherfield.notify.class", $config["class"]);
        $container->setParameter("lrotherfield.notify.type", $config["type"]);
        $container->setParameter("lrotherfield.notify.lifetime", $config["lifetime"]);
        $container->setParameter("lrotherfield.notify.click_to_close", $config["click_to_close"]);

        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yml');
    }

The above code creates a new instance of our Configuration class and then processes the TreeBuilder object and returns an array of our nodes. We have then used the array to set parameters in the container. Now we just inject them in our services.yml file:

#services.yml
#...
lrotherfield.notify:
  class: "%lrotherfield.notification.class%"
  arguments:
  session: @session
  defaults:
    message: %lrotherfield.notify.message%
    title: %lrotherfield.notify.title%
    class: %lrotherfield.notify.class%
    type: %lrotherfield.notify.type%
    lifetime: %lrotherfield.notify.lifetime%
    click_to_close: %lrotherfield.notify.click_to_close%

Finally

Lets use these defaults in our NotificationController:

//NotificationController
//...
    private $defaults = array(),
        $flashes = array(),
        $session;

   /**
    * @param \Symfony\Component\HttpFoundation\Session\Session $session
    * @param array $defaults
    */
    public function __construct(\Symfony\Component\HttpFoundation\Session\Session $session, array $defaults)
    {
        $this->session = $session;
        $this->defaults = $defaults;
    }

So now, rather than just the default type, we have all of our defaults that can be overwritten in config.yml or by passing them in the arguments array.

Lets test it

Using the default index.html.twig template, we want to extend the base.html.twig template (assuming the base template is where you mark up the basic HTML structure like html, head and body tags):

{# Default/index.html.twig #}
{# remove the old test loop #}
{% extends “::base.html.twig” %}

Then in base.html.twig we can render the resources and the notifications:

{# base.html.twig #}
{# ... #}
  {{ notify_resources() }}
  </head>
  {# ... #}
  <div id=”notify_container”></div>
  {# ... #}
  {{ notify_all(“notify_container”) }}
  </body>

Notice above, although we could have used notify_all() with no arguments, we have passed through “notify_container”. This makes use of the container variable we set up ages ago to render the notifications inside the element with the id notify_container which adds a nice final level of customisability to our bundle. If you get any errors, it is worth clearing your cache and trying again:

app/console cache:clear

Thanks for reading, I hope this 2 part series helped with your understanding of Symfony2, particularly Dependency Injection and services in Symfony2. If you notice any bugs in the above, please let me know using the comment form below. I have read and re-read both articles but its over 3600 words so at least one mistake is likely :S

Luke Rotherfield

Symfony2 Developer in CT USA. Luke is a Symfony2 wizard and has written some sweet libraries of his own. Luke loves Jesus, his gorgeous wife and his two beautiful daughters :)