Symfony2: More on Twig Ajax Templates

This post is a few notes to follow up on some of the responses to my previous post on reusing inner Twig templates for full page responses and ajax responses.

One clarification to make, as I had not made it clear, is that is only relevant to the case where you are sending back HTML fragments is response to Ajax requests (and where that fragment is the same as the full page response minus the common parts of the page). If your Ajax response is XML or JSON then this technique is irrelevant as you will already be using different templates. In that case you can switch template using the _format route parameter or you may want to look at using the FOSRestBundle.

Thomas Rabaix of the Sonata Project pointed out that a further improvement can be made by separating out the names of the templates to be extended into configuration by using Twig globals. This means that the choice of template to extend is no longer hardcoded into the templates at all. In a Symfony2 app it is easy to register globals in config. Our simple intermediate template that decides whether to extend the full or ajax template would now look like this:

{# app/Resources/views/layout.html.twig #}
{% extends app.request.xmlHttpRequest 
         ? acme.templates.full
         : acme.templates.partial %}

with the global parameters set in the app's main config file:

# app/config/config.yml
twig:
    globals:
        acme:
            templates:
                full: "::ajax-layout.html.twig"
                partial: "::full-layout.html.twig"

I also mentioned moving any logic for deciding which template to use to a Twig extension, if it became more complex than our simple ternary operator, so that it could be tested outside of a template. This could return the template to use rather than just a boolean allowing more complex decisions to be made as well.

A simple version of the extension may look like this:

namespace Acme\DemoBundle\Twig;

class DemoExtension extends \Twig_Extension
{
    const FULL_TEMPLATE = '::full-layout.html.twig';
    const PARTIAL_TEMPLATE = '::partial-layout.html.twig';

    public function getFunctions()
    {
        return array(
            'parent_template' => new \Twig_Function_Method($this, 'getParentTemplate'),
        );
    }

    public function getParentTemplate()
    {
        if ($this->useFullTemplate()) {
            return self::FULL_TEMPLATE;
        }
        return self::PARTIAL_TEMPLATE; 
    }

    public function useFullTemplate()
    {
         //...
    }
}
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
         <service id="acme.twig.demo_extension" 
                  class="Acme\DemoBundle\Twig\DemoExtension">
             <tag name="twig.extension" />
         </service>
     </services>
</container>

We can now remove the intermediate template altogether and call our twig function from the inner template:

{# src/Acme/DemoBundle/Resources/views/Product/new.html.twig #}
{% extends parent_template() %}

{% block title %}New product{% endblock %}

{% block body %}
<form action="{{ path('product_new') }}" method="post" {{ form_enctype(form) }}>
    {{ form_widget(form) }}

    <input type="submit" />
</form>
{% endblock body %}

In this example the templates are hardcoded in as class constants to keep the example straightforward, this could be changed to have them injected in to the extension so that they can be kept in configuration instead.