Search

Rss Posts

Rss Comments

Login

 

Dynamic layouts in Twig

Dec 17

Recently I worked on a project that needed dynamic layouts for each bundle.
The idea was to have a base template, then several layout templates that each extends from the base.
Each view would then extend from one of the layout files.

So this is how it worked. The base template looked like this:

<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
    {% block stylesheets %}{% endblock stylesheets %}
</head>
<body>
    <section class="container">
        <div align="center">
        {{ block("header") }}
        <section class="content">
            <div class="row">
                <div class="col-sm-12">
                    {{ block("body") }}
                </div>
            </div>
        </section>
    </section>
    {% block javascripts %}{% endblock javascripts %}
</body>
</html>

And this is an example of one of the layout templates:

{% extends "::base.html.twig" %}
{% block body %}
    <div class="row">
        <div class="col-lg-3">
            {{ block("sidebar") }}
        </div>
        <div class="col-lg-9">
            {{ block("content") }}
        </div>
    </div>
{% endblock body %}

I have a couple of layouts in my project, full_width.html.twig, left_column.html.twig, three_column.html.twig etc.
And all my views extends one of these layouts.

As you might have notices in the base template, there is blocks for javascripts and stylesheets. In the scenario I had, I needed to allow each bundle to specify there own stylesheets and javascripts. So each bundle needed to create a custom layout that overrides the parent stylesheet and javascript block adding their own content, and I did not want to re-create the layout files for each bundle so the view can extend from them, because it will get difficult to manage if you constantly add new custom bundles, or if you make a change to one of the layouts and that change needs to be reflected in every layout file.

So I opted for a way to dynamically create a custom base file for each bundle, that can extend any layout and override any of the parent blocks as necessary.

The first part was to create a new Twig function that will dynamically call a custom base file.

<?php
namespace Acme\CoreBundle\Twig\Extension;
use Symfony\Component\HttpFoundation\RequestStack;
class LayoutExtension extends \Twig_Extension
{
    protected $request;
    protected $env;
    public function __construct(RequestStack $request)
    {
        $this->request = $request;
    }
    public function getFunctions()
    {
        return array(
            new \Twig_SimpleFunction('layout', array($this, 'getLayout'))
        );
    }
    public function initRuntime(\Twig_Environment $environment)
    {
        $this->env = $environment;
    }
    public function getLayout($layoutName = null)
    {
		// ...
    }
    public function getName()
    {
        return 'layout_extension';
    }
}

And then register the extension in your services.yml file:

 layout.twig.extension:
    class: Acme\CoreBundle\Twig\Extension\LayoutExtension
    arguments: [@request_stack]
    tags:
        - { name: twig.extension }

So now I need to define my core layouts, and my custom layouts for each bundle.

For that I added some configuration options to my core bundle, to be able to specify the layouts in the config.yml.

namespace Acme\CoreBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('mi_way_core');
        $rootNode->children()
                    ->arrayNode('layouts')
                        ->addDefaultsIfNotSet()
                        ->children()
                            ->arrayNode('core')
                                ->useAttributeAsKey('name')
                                ->prototype('scalar')->end()
                            ->end()
                            ->arrayNode('bundles')
                                ->useAttributeAsKey('name')
                                ->prototype('scalar')->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
                ;
        return $treeBuilder;
    }
}

And Then we need an extension class that will add our templates from the config to the twig extension;

namespace Acme\CoreBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
class AcmeCoreExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $config = $this->processConfiguration(new Configuration(), $configs);
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yml');
        $layouts = $config['layouts'];
        $layoutExtension = $container->getDefinition('layout.twig.extension');
        $layoutExtension->addMethodCall('setCoreTemplates', array($layouts['core']));
        $layoutExtension->addMethodCall('setBundleTemplates', array($layouts['bundles']));
    }
}

So as you can see, there is 2 config options. The first one is the core template files.
This allows me to specify my main layout temnplates.

The other one is the base template for each bundle. With the option, I can set a base for all my bundles, which each template will each, and the base will inherit from the core layout files.

So let’s add the necessary methods to our twig extension:

namespace Acme\CoreBundle\Twig\Extension;
use Symfony\Component\HttpFoundation\RequestStack;
class LayoutExtension extends \Twig_Extension
{
    protected $coreTemplates = array();
    protected $bundleTemplates = array();
    // ...
    public function setCoreTemplates(array $templates)
    {
        $this->coreTemplates = $templates;
    }
    public function setBundleTemplates(array $templates)
    {
        $this->bundleTemplates = $templates;
    }
    // ...
}

Now we need to add the logic for the layout function. This will check if the current bundle you are using have a custom base layout.
If it does, it will return that template as the parent for the current view, and set the layout you want to use as the parent for the base template.

< ?php
namespace Acme\CoreBundle\Twig\Extension;
class LayoutExtension extends \Twig_Extension
{
    // ...
    public function getLayout($layoutName = null)
    {
        if(empty($layoutName)) {
            throw new \InvalidArgumentException('Template layout can not be empty');
        }
        if(!array_key_exists($layoutName, $this->coreTemplates)) {
            throw new \InvalidArgumentException(sprintf('Template %s is invalid. Available values are: "%s"', $layoutName, implode(', ', array_keys($this->coreTemplates))));
        }
        $request = $this->request->getMasterRequest();
        $controller = $request->get('_controller');
        $namespace = strtolower(strstr($controller, '\\', true));
        if(isset($this->bundleTemplates[$namespace])) {
            $this->env->addGlobal('layout', $this->coreTemplates[$layoutName]);
            $layout = $this->bundleTemplates[$namespace];
            if($this->env->getLoader()->exists($layout)) {
                return $layout;
            }
        }
        return $this->coreTemplates[$layoutName];
    }
    // ..
}

And then finally we need to specify the our layouts in the config.yml:

acme_core:
    layouts:
        core:
            full_width: AcmeCoreBundle:Layout:full_width.html.twig
            left_column: AcmeCoreBundle:Layout:left_column.html.twig
            right_column: AcmeCoreBundle:Layout:right_column.html.twig
            three_column: AcmeCoreBundle:Layout:three_column.html.twig
        bundles:
            user: AcmeUserBundle::base.html.twig

So how does this work exactly?
Well, in your view, you need to use E.G {% extends layout('full_width') %} as your parent layout.
This will check in which bundle you are currently, then check if there is a base layout for that bundle available.
If a base layout is available, it will return that file as your parent, and set a dynamic layout variable in twig, which will contain your core template name (AcmeCoreBundle:Layout:full_width.html.twig in this case).

So just make sure your base layout extends the dynamic layout variable.

Usage example:

{% extends layout('full_width') %}
{% block content %}
    {# your view content here #}
{% endblock content %}

And then as you base template:

{% extends layout %}
{% block stylesheets %}
    {{ parent() }}
    {% stylesheets "@custom_bundle_css" %}
        
    {% endstylesheets %}
{% endblock stylesheets %}
{% block javascripts %}
    {{ parent() }}
    {% javascripts "@custom_bundle_js" %}%MINIFYHTML3bf3da25e3e6c58cf1fb5e6f47ed66ed4%{% endjavascripts %}
{% endblock javascripts %}

So you can have dynamic layouts for every bundle by just modifying the base bundle file and overwriting any block from the parent layout that you need.

2 Comments

Add your comment

  1. […] they’ve shared a new tutorial showing you how to use the popular PHP templating tool Twig to create dyanmic layouts for your […]

  2. […] blog they've shared a new tutorial showing you how to use the popular PHP templating tool Twig to create dyanmic layouts for your […]

Post a comment

*

 

Page optimized by WP Minify WordPress Plugin