how to implement a database driven navigation into your layout within symfony4

Today we are implementing a database driven navigation into a Symfony application. My prefered way to complete this task is to create a Twig Extension in combination with LazyLoading to prevent overhead and initiation of unused classes. With this solution we have our query logic inside the repositories and the markup inside a Twig template to ensure a clean separation. As an alternative approach you could register your navigation as a service or simply render a controller method inside your Twig template.

install Symfony MakerBundle

I suggest you install The Symfony MakerBundle into your project to enable some awesome console commands that will make your day easier. If you haven't installed it already you can download and install the bundle with one simple command in your console.

$ composer require symfony/maker-bundle --dev

create a Twig Extension

To create a Twig Extension we can use the MakerBundle. For our example we will create one called NavigationExtension with the following command inside the console:

$ ./bin/console make:twig-extension NavigationExtension

The MakerBundle creates a class inside /src/Twig/

# /src/Twig/NavigationExtension.php
namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;

class NavigationExtension extends AbstractExtension
{
    public function getFilters(): array
    {
        return [
            // If your filter generates SAFE HTML, you should add a third
            // parameter: ['is_safe' => ['html']]
            // Reference: https://twig.symfony.com/doc/2.x/advanced.html#automatic-escaping
            new TwigFilter('filter_name', [$this, 'doSomething']),
        ];
    }

    public function getFunctions(): array
    {
        return [
            new TwigFunction('function_name', [$this, 'doSomething']),
        ];
    }

    public function doSomething($value)
    {
        // ...
    }
}

register our Navigation as TwigFunction

We only need the getFunctions() method since we are not creating any filters inside our Twig Extension so we can remove the other methods and remove the use statement for TwigFilter. In addition to those changes we register our new TwigFunction inside getFunctions(), name it "main_navigation" and create the method that should be called:

# /src/Twig/NavigationExtension.php
namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class NavigationExtension extends AbstractExtension
{
    public function getFunctions(): array
    {
        return [
            new TwigFunction('main_navigation', [$this, 'getNavigation']),
        ];
    }

    public function getNavigation()
    {
        // ...
    }
}

Dependency Injection

At getNavigation() we can use dependency injection to get access to a repository which holds the logic for the navigation and inject the TwigEnvironment we need to render it. Don't forget to add the use statements.

In this example we put the query logic into an ArticleRepository.

# /src/Twig/NavigationExtension.php
namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use App\Repository\ArticleRepository;
use Twig\Environment;

...

    public function getNavigation(ArticleRepository $articleRepository, Environment $twig)
    {
        $navigation = $articleRepository->getNavigation();
        return $twig->render('/main-navigation.html.twig', [
           'navigation' => $navigation
        ]);
    }

...

call our TwigFunction inside a template

We now have access to our new TwigFunction inside our templates and can call it with {{ main_navigation() }}. Let's add it into our layout:

#/templates/base.html.twig

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>{% block title %}{% endblock %}
        {% block stylesheets %}{% endblock %}
    </head>
    <body>
        {{ main_navigation() }}
        <main>
            {% block body %}{% endblock %}
        </main>
        {{ include('footer.html.twig') }}
        {% block javascripts %}{% endblock %}
    </body>
</html>

implement ServiceSubscriberInterface

Now we already have a working Twig Extension but there is still a problem with this solution: Everytime Twig is used within the Symfony application our extension is loaded and the dependencies are instantiated. To prevent this behavior we will use a concept called LazyLoading. To enable this we have to implement an Interface called ServiceSubscriberInterface provided by Symfony.

The Interface forces us to implement a static method called getSubscribedServices which has to return an array that includes required service types keyed by service names. To manage the dependencies we use the ContainerInterface.

Let's take a look at our final class:

# /src/Twig/NavigationExtension.php
namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use App\Repository\ArticleRepository;
use Twig\Environment;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

class NavigationExtension extends AbstractExtension
{
    private $container;

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

    public function getFunctions(): array
    {
        return [
            new TwigFunction('main_navigation', [$this, 'getNavigation']),
        ];
    }

    public function getNavigation()
    {
        $articleRepository = $this->container->get(ArticleRepository::class);
        $twig = $this->container->get(Environment::class);
        $navigation = $articleRepository->getNavigation();
        return $twig->render('/main-navigation.html.twig', [
           'navigation' => $navigation
        ]);
    }

    public static function getSubscribedServices()
    {
        return [
            ArticleRepository::class,
            Environment::class
        ];
    }
}

This solutions ensures that our dependencies only get instantiated when we call {{ main_navigation() }} inside a template and not everytime we use Twig. This website here also uses this solution to create the navigation. You can take a look on the extension on github.

Twig Extensions can be used in many ways and can simplify a lot of your processes related to Twig.

comment successfully committed

leave a comment:

please enter your first name above
please enter a valid email above
please enter your message before you send the form

connect

please enter your name
please enter a valid email above
please enter the subject for your request
please enter your message before you send the form
mail successfully committed