Search

Rss Posts

Rss Comments

Login

 

Dynamic layouts in Twig

Dec 17 2013

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" %}%MINIFYHTML47db9270d92bcc064c9bc30ac3b831d42%{% 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.

Google IO 2013

May 13 2013

Deploying Symfony applications with Capifony

Oct 01 2012

I recently needed to deploy a Symfony application to a production server.

After some searching, I noticed Capifony.
Capifony is an extension of Capistrano, specifically built for Symfony projects.

For those who don’t know Capistrano, it is a deployment utility build in ruby, more specifically for ruby projects, but you get a few extensions that you can use to deploy PHP applications as well.
Here is an excerpt about Capistrano: “Capistrano is a utility and framework for executing commands in parallel on multiple remote machines, via SSH….Capistrano is a utility and framework for executing commands in parallel on multiple remote machines, via SSH”

So the first part of setting up Capifony is to create an SSH connection to the production server. For that I created a public ssh key without a password that I just copied over to the server, so this allows me to connect to the production server from my local machine without having to worry about entering a password every time, and this also makes it easier for Capifony to connect to the server to deploy the application.

On the production server, I needed to make sure that it can connect to my private Github account, which is also accomplished using SSH, so that the latest code can be cloned to the machine.
The last step was just to create the deployment configuration, which look like the following:

set :application, "Application Name"
set :domain,      "domain_name"
set :deploy_to,   "/var/www/html"
set :app_path,    "app"
set :deploy_via, :remote_cache
set :repository,  "github-repository"
set :scm,         :git
# Or: `accurev`, `bzr`, `cvs`, `darcs`, `subversion`, `mercurial`, `perforce`, or `none`
set :model_manager, "doctrine"
# Or: `propel`
role :web,        domain                         # Your HTTP server, Apache/etc
role :app,        domain                         # This may be the same as your `Web` server
role :db,         domain, :primary => true       # This is where Symfony2 migrations will run
set  :keep_releases,  3
set :shared_files,      ["app/config/parameters.yml", "app/environment.txt"]
#set :shared_children,     [app_path + "/logs", web_path + "/uploads", "vendor"]
set :shared_children,     [web_path + "/uploads", "vendor"]
set :use_composer, true
set :update_vendors, true
set :update_assets_version, true
set :dump_assetic_assets, true
set :interactive_mode, false
set :writable_dirs,     ["app/cache", "app/logs"]
set :webserver_user,    "apache"
set :permission_method, :acl
set :interactive_mode, false
set :use_sudo, false
set :user, "root"
default_run_options[:pty] = true
# Be more verbose by uncommenting the following line
logger.level = Logger::MAX_LEVEL
# Custom Tasks
after "symfony:cache:warmup", "deploy:set_permissions" # sets the required folders to writable
after "deploy:set_permissions", "permission:setowner" # Change ACL on the app/logs and app/cache directories
# Sets the directory to the correct owner
namespace :permission do
  task :setowner do
      capifony_pretty_print "--> Change folder to owner #{webserver_user}"
      run "chown -R #{webserver_user}: #{latest_release}/"
      puts_ok
  end
end

And that’s it. So each time I need to update changes, I just use the command

cap deploy

, then all the latest code is pushed to the server.
Capifony also keeps a history of the previous 5 releases, so if something went wrong on deployment, I can just roll back to the previous version.

 

Page optimized by WP Minify WordPress Plugin