Multiple domains (or sub-domains) Twig templating using Symfony2 event listener

Posted on

Introduction

While developing single Symfony2 application on multiple domains for example e-commerce with multiple stores that are using same controller actions or any other common usage, you will probably need to change template folder according to domain or sub-domain.

In our example

we consider 2 websites on different domains that represents two websites

site1.com
site2.com

How Twig Loader works

Most of the applications works with single Twig Loader (default) where all template folders and names spaces are registered and available to be loaded by your controllers, by default all the project bundles are registered automatically in your Twig loader and accessible thought direct access with physical path

$this->render('path/to/my/twig');

or by namespace (commonly used in Symfony2)

$this->render('AcmeDemoBundle:Demo:index.html.twig');

or

$this->render('@AcmeDemo/Demo/index.html.twig');

in order to achieve using different templating (themes) folders you will have two options :

  • To change the template directory paths of current Twig Loader by adding new directory paths.
  • Create new custom Twig Loader to work along with the existing one, that has your custom directory paths and structure.

We will go through the first option is to add paths of the current TwigLoader

Configuration:

We will have to set some configuration for the the two stores

if you are not familiar with custom configuration you can check Symfony2 docs here
in /src/Acme/DemoBundle/Resources/config/config.yml

acme_demo:
   sites:
      site1:
         domain: site1.com
         template_directory: Site1
      site2:
         domain: site2.com
         template_directory: Site2

and some configuration validation

in /src/Acme/DemoBundle/DependencyInjection/Configuration.php

<?php 
namespace Acme\DemoBundle\DependencyInjection; 
use Symfony\Component\Config\Definition\Builder\TreeBuilder; 
use Symfony\Component\Config\Definition\ConfigurationInterface; 
/**  * This is the class that validates and merges configuration from your app/config files  *
     * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}  
*/ 
class Configuration implements ConfigurationInterface {     
/**      
* {@inheritDoc}      
*/     
public function getConfigTreeBuilder()     {         
$treeBuilder = new TreeBuilder();         
$rootNode = $treeBuilder--->root('acme_demo');
        $rootNode
                ->children()
                    ->arrayNode('sites')
                    ->prototype('array')
                        ->children()
                            ->scalarNode('domain')->isRequired()->cannotBeEmpty()->validate()->ifNull()->thenInValid("Domain name must be set")->end()->end()
                            ->scalarNode('template_directory')->isRequired()->cannotBeEmpty()->validate()->ifNull()->thenInValid("Template Directory must be set")->end()->end()
                        ->end()
                    ->end()
                ->end()
        ;

        // Here you should define the parameters that are allowed to
        // configure your bundle. See the documentation linked above for
        // more information on that topic.

        return $treeBuilder;
    }
}

and in /src/Acme/DemoBundle/DependencyInjection/AcmeDemoExtension.php

<?php namespace Acme\DemoBundle\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Config\FileLocator; class AcmeDemoExtension extends Extension {     public function load(array $configs, ContainerBuilder $container)     {         $configuration = new Configuration();         $config = $this--->processConfiguration($configuration, $configs);
        $sites = array();
        //Re arrange the sites configuration to be easily accessed by domain as index
        foreach ($config['sites'] as $siteConfig) {
            $sites[$siteConfig['domain']]['template_directory'] = $siteConfig['template_directory'];
        }
        $container->setParameter('hosts.config', $sites);
        $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
        $loader->load('services.yml');

    }

    public function getAlias()
    {
        return 'acme_demo';
    }
}

Template listener:

Now we have hosts configuration ready to be injected in the templateListener

First we need to listen on kernel.controller event

in /src/Acme/DemoBundle/Resources/config/services.yml

services:
  acme.template_listener:
    class: Acme\DemoBundle\EventListener\TemplateListener
    arguments: ["@twig",%hosts.config%]
    tags:
         - { name: kernel.event_listener, event: kernel.controller, method: onKernelController }

Finally the template we will write some code that will take place just before the controller call to change the template folder according to the website

in /src/Acme/DemoBundle/EventListener/TemplateListener.php

<!--?php namespace Acme\DemoBundle\EventListener; use Symfony\Component\HttpKernel\Event\FilterControllerEvent; class TemplateListener {     /** @var \Twig_Environment */     private $twig;     /**      * @var array      */     private $hostsConfig;     public function __construct(\Twig_Environment $twig, $hostsConfig)     {         $this--->twig = $twig;
        $this->hostsConfig = $hostsConfig;
    }

    public function onKernelController(FilterControllerEvent $event)
    {
        $currentHost = $event->getRequest()->getHost();
        $this->twig->getLoader()->addPath(dirname(__DIR__) . '/../DemoBundle/Resources/views/Common','Default');
        $this->twig->getLoader()->prependPath(dirname(__DIR__) . '/../DemoBundle/Resources/views/' . $this->hostsConfig[$currentHost]['template_directory']);
    }
}

considering that we have the below template structure inside the views folder

Mutli site views

Views folder structure

the above code will check what is the current domain, and change the twigLoader paths to the current website template folder

Note: the precedence of adding paths matters and define which folder twig loader will look at first, if you want to change the orders you can change precedence or use prependPath

$this->twig->getLoader()->prependPath(dirname(__DIR__) . '/../DemoBundle/Resources/views/' . $this->hostsConfig[$currentHost]['template_directory']);

Then you can simple load twig as below

<!--?php namespace Acme\DemoBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; class HomeController extends Controller {     /**      * @Route("/", name="_demo")      */     public function indexAction()     {         return $this--->render('index.html.twig');
    }
}

Final thoughts:

  • You can use check for subdomain , user agent or anything else that suit your project
  • You can use namespace second parameter provided by addPath and prependPath functions as below

like this

$this->twig->getLoader()->addPath(dirname(__DIR__) . '/../DemoBundle/Resources/views/Common','Default');

And in the loading

$this->render('@Default\index.html.twig');

Tags: , ,


  • Jörg Frintrop

    Hi

    It works great.
    But how is it working with assets for css and js. It would be nice, if they are also loaded from die site folder

    Reply

  • rick

    FatalErrorException: Error: Call to undefined method Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition::scalarNode() in /home/six/top82/src/AppBundle/DependencyInjection/Configuration.php line 16

    $rootNode
    ->children()
    ->arrayNode(‘sites’)
    ->prototype(‘array’)
    ->children()
    ->scalarNode(‘domain’)->isRequired()->cannotBeEmpty()->validate()->ifNull()->thenInValid(“Domain name must be set”)->end()
    ->scalarNode(‘template_directory’)->isRequired()->cannotBeEmpty()->validate()->ifNull()->thenInValid(“Template Directory must be set”)->end()
    ->end()
    ->end()
    ->end()
    ->end()
    ;

    giving error to the second scalarNode(‘template_directory’). what could be the reason

    Reply

  • rick

    The service “template_listener” has a dependency on a non-existent parameter “hosts.config”.

    Reply

  • David

    I’m getting a “Cannot redeclare class …\TemplateListener” error – not sure why it’s trying to declare it twice

    Reply

  • Chadwick Meyer

    Also, I don’t know if it matters, but I bypassed all the special config for hard coded values because I needed to do this dynamically based on the current site. So I just set the alternative path directly in my controller (after I know which template the current site is using):

    $this->container->get(‘twig.loader’)->prependPath(__DIR__.’src/Templates/GutensiteLunarBundle/Resources/views’, ‘template’);

    Reply

    • Ahmed Samy Post author

      Hey Chatwick, generally i am not sure you are heading to the right solution for your specific case
      how much code is shared by these different websites ?
      if it’s a lot of code you may consider bundle inheritance instead, it will cover template overriding just as normal Symfony2 way and it will allow you to use assets without any hacks and same as any other parts too

      so you can have different apps (sites) every app has several bundles

      Finally this approach really helps when you have multiple websites that are using same widgets and pages, but it’s not good to work along side with bundle inheritance

      Hope this answer helped you in anyway

      Reply

  • Chadwick Meyer

    I am building a hosted CMS platform that will have a lot of different sites as well, each will either use one of our templates or even a custom template that will need to be stored outside the symfony project root in their own vhost folder… so thanks for these ideas.

    1. Where do you store the assets (Resources/public)? Do you put in a similar path `Resources/public/Site1` so that assets:install –symlinks will link to them as well? I had trouble with assets when I specified an alternative template directory.
    See this question and this one too.

    2. I ended up creating a special `Templates` vendor with different theme bundles, and then each site has a setting which specifies which template to use. This lets me overwrite core controllers as well as templates. But I don’t yet have a solution for the assets in custom client vhost paths.
    See this question.

    The problem that I still face is the need to override the templates of other bundles (e.g. a template may need a special way to display the ArticleBundle). Any ideas would be much appreciated.

    Reply

  • Vincent Bergeron

    Finally, I made a FileLocator class and tell Twig to use it instead. Much much more easier.

    Reply

  • Vincent Bergeron

    I followed the instructions and it`s not working for me.

    I’m using the latest version of Symfony. The views are all loaded from the default folder (Resources/views).

    I cleared the cache, putted a die(`here`); in the constructor of the TemplateListener. Then I looked in the appDevDebugProjectContainer.php, in the getTwig_LoaderService and the default views folder is listed there.

    So this file is being constructed before the TemplateListener gets called. I think that`s why my addPath and prependPath is ignored.

    Maybe there`s something different with the version I`m using… I would really like to use this method for my theming need.

    Can sombody help me?

    Thanks

    Reply

  • Guillermo

    Hi, thank you for your post. I have a question. If you have more bundles in your src directory, How implement this listener? Sorry for my english

    Reply

    • Ahmed Samy Post author

      if you have more than one bundle with view files it should work normally, as this listener adds paths to look at directly but does not remove any , so other bundles views loadable normally with their namespaces or direct path

      I hope that answered your question

      Reply

      • Pepo

        Actually this is interesting
        I want to have more domains deployed inside a single Symfony installation but I want for each domain a different bundle which will be handling it (routes, views, models etc) – is this possible, or I understand Symfony a bit weirdly?
        Is it a good idea to actually use a single instance of Symfony for more different “sites” (I’m thinking single sign on as an example)

        Reply

        • Ahmed Samy Post author

          It really depends on the business model, how much common features shared between different sites , but this post mainly resolve the problem with different templates (twig files) according to domain or sub-domain

          but at some point you may need to use different app inside the project

          Reply

  • Essam khaled

    Thank you, good job (y)

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Current day month ye@r *