Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to include a reusable widget in Symfony (Twig)?

So, I'm still fairly new to Symfony and Twig. I was wondering how to best include/create a snippet of reusable code in the templates. Say, for example, that you have a sidebar that you want to show on every page.

{% extends 'AppBundle::base.html.twig' %}

{% block body %}
    <div id="wrapper">
        <div id="content-container">
            {# Main content... #}
        </div>
        <div id="sidebar">
            {% include 'sidebar.html.twig' %}
        </div>
    </div>
{% endblock %}

And that in that sidebar are a couple of widgets that all do their own logic. How you do go about creating/including those widgets?

So far, I've come across several solutions.

As a controller

The first was to embed the widget as a controller(s) in Twig.

class WidgetController extends Controller
{
    public function recentArticlesWidgetAction()
    {
        // some logic to generate to required widget data
        // ...

        // Render custom widget template with data
        return $this->render('widgets/recentArticles.html.twig', array('data' => $data)
        );
    }
    public function subscribeButtonWidgetAction()
    {
        // ...

        return $this->render('widgets/subscribeButton.html.twig', array('data' => $data)
    }

    // Many more widgets
    // ...
}

And include that in 'sidebar.html.twig' like so

<div id="sidebar">        
    {# Recent Articles widget #}
    {{ render(controller('AppBundle:Widget:recentArticlesWidget' )) }}

    {# Subscribe-Button widget #}
    {{ render(controller('AppBundle:Widget:subscribeButtonWidget' )) }}

    {# and so on #}
</div>

As a service

I've also seen some people register widgets as services (that can be used in Twig directly). With the widget main class

// src/AppBundle/Service/RecentArticlesWidget.php
namespace AppBundle\Service;

use Symfony\Component\DependencyInjection\ContainerInterface;

class RecentArticlesWidget
{
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function getRecentArticles()
    {
        // do some logic (use container for doctrine etc.)
    }
}

that is then registered as a service,

# src/AppBundle/Resources/config/services.yml
services:
    recentArticlesWidget:
        class:     AppBundle\Service\RecentArticlesWidget
        arguments: ["@service_container"]

passed to the template in the controller,

namespace AppBundle\Controller;

class SidebarController {

    public function showAction($request) {

        // Get the widget(s) 
        $recentArticlesWidget = $this->get('recentArticlesWidget'); 

        // Pass it (them) along
        return $this->render('sidebar.html.twig', array('recentArticlesWidget' => $recentArticlesWidget));
    }
}

so it can simply be used like this in Twig

{# sidebar.html.twig #}

{{ recentArticlesWidget.getRecentArticles()|raw }}

Alternatively, you can also add your service to the Twig global variables directly by adding it to the Twig config. This way, it won't need to be passed into the view by the controller.

#app/config/config.yml
twig:
    globals:
        # twig_var_name: symfony_service
        recentArticlesWidget: "@recentArticlesWidget"

As a Twig Extension

This one is very similar to using a service above (see the documentation). You create an a twig extension class that is almost identical to the service shown previously

// src/AppBundle/Twig/RecentArticlesWidgetExtension.php
namespace AppBundle\Twig;

class RecentArticlesWidgetExtension extends \Twig_Extension
{
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function getFunctions()
    {
        return array( 
            "getRecentArticles" => new Twig_Function_Method($this, "getRecentArticles")
            // register more functions
        );
    }

    public function getRecentArticles()
    {
        // do some logic (use container for doctrine etc.)
    }

    // Some more functions...

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

Register that as a service with an added tag

# src/AppBundle/Resources/config/services.yml
services:
    recentArticlesWidget:
        class: AppBundle\Twig\RecentArticlesWidgetExtension
        arguments: [@service_container]
        tags:
            - { name: twig.extension }

and simply use it like a global function in Twig

{# sidebar.html.twig #}

{{ getRecentArticles() }}

Thoughts

One thing I noticed is that with the last two methods is that the logic and the view don't seem to be seperated at all anymore. You basically write a widget function and have that function output the complete html for the widget. This seems to go against the modularity and patterns Symfony tries to enforce.

On the other hand, calling a distinct controller or controller action (with their own twig renders) for every single widget seems like it could take more processing than might be needed. I'm not sure if it actually slows anything down, but I do wonder if its excessive.

Long story short, is there a best practice for using reusable widgets in Symfony? I'm sure some of these methods can also be mixed, so I was just wondering how to best go about this.

like image 851
Mvin Avatar asked Jan 22 '17 14:01

Mvin


1 Answers

Twig extension and Twig macro should point you in the right direction.

Use the macro for the view and extension for the business logic.

On a side note in your Twig extension example, it's probably a good idea to only pass in services that you are using instead of the whole service container.

like image 101
ClickLabs Avatar answered Sep 21 '22 23:09

ClickLabs