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

3

10

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.

Polley answered 22/1, 2017 at 14:25 Comment(0)
J
1

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.

Jonellejones answered 23/1, 2017 at 15:56 Comment(0)
C
-1

I would rather use blocks and a parent template. Simply put, insert the side bar in the main layout and have all other templates that require the side bar inherit from it.

Something like this:

layout.html.twig will be something like this:

{% block title}
// title goes here
{%endblock%}

<div id="wrapper">
    <div id="content-container">
        {% block pageContent %}
        {% endblock %}
    </div>
    <div id="sidebar">
        // Side bar html goes here
    </div>
</div>

Now all pages will inherit from this layout.html.twig. Say for example a page called home.html.twig will be:

home.html.twig

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

{% block title%}
// this page title goes here
{% endblock %}

{% block pageContent %}
    //This page content goes here
{% endblock %}

You can add as many blocks as needed, for example css and js blocks for each page.

Hope this helps!

Capapie answered 23/1, 2017 at 8:16 Comment(1)
Thank you for your answer! What you propose is helpful and correct. However, my question was more along the lines of how to best put complex code in the sidebar (or anywhere else) in the twig template. As in, content that needs to do its own queries and has all kinds of logic to generate an output. Stuff that wouldn't normally be passed along in the main controller. I took widgets as a simple example (fetch me the three most popular articles and display them nicely). I personally tend to embed additional "widget controllers" as described above, but I was wondering what everybody else did.Polley
N
-1

I think the simplest way is defining a block in a template and then extending that template to render blocks like so:

#reusable.html.twig

{% block reusable_code %}
  ...
{% endblock %}

And

#reused.html.twig
{% extends 'reusable.html.twig' %}

{{ block('reusable_code') }}

If you want more reusability than that or your block contains business logic or model calls a twig extension is the way to go

Nicaea answered 23/1, 2017 at 8:43 Comment(2)
He is talking about using logic data in that widget, in a way that he does not have to send that data in every controller response.Delmore
"If you want more reusability than that or your block contains business logic or model calls a twig extension is the way to go" i should have added an exempleNicaea

© 2022 - 2024 — McMap. All rights reserved.