Create custom TwigExtension


Warning: This article was wrote on early Drupal 8.2.x version. Twig service also its class base had changed nonetheless it still has same implementation logic.


Drupal 8 ships with Twig as engine to manage templates, and it provides us a TwigExtension in the core:

TwigExtension class allows to do almost everything within Twig templates, but what if we need to go far away, and what we really need isn't available out-of-the-box, then we could create our own TwigExtension, we could achieve it by follow Symfony documentation.

Create custom module

I won't reinvent the wheel, there is a lot of places where you could see about how to create a custom module in Drupal 8, then we will do the same, I recommend to use Drupal Console by type in drupal generate:module,

drupal:console:generate:module

Create service

Drupal creates a service to expose Core Twig Extension, then guess what, we will do the same, and thanks gods we don't need to memorize how to create a service inside Drupal, because Drupal console does it for us, just type in drupal generate:service,

drupal:console:generate:service

In this case, it will create a new services class named: "UtilityTwigExtension", we go to make magic there, but before that happen, we should move our new file into "TwigExtension" folder, inside "src" folder of our "Utility" module:

TwigExtensionFolder

Also we need to modify "utility.service.yml" in order to define new path to our "UtilityTwigExtension.php" file,

utility.module

If you noticed, we only add "TwigExtension" between "utility" and "UtilityTwigExtension", since Drupal will check for that folder structure.

Extends from Core TwigExtension

Firstly we need to define the proper namespace to our "UtilityTwigExtension" class and be totally sure that it extends from "TwigExtension" class which Drupal ships in the core,


namespace Drupal\utility\TwigExtension;

use Drupal\Core\Render\Renderer;
use Drupal\Core\Template\TwigExtension;

/**
 * Class UtilityTwigExtension.
 *
 * @package Drupal\utility
 */
class UtilityTwigExtension extends TwigExtension {

  /**
   * Drupal\Core\Render\Renderer definition.
   *
   * @var \Drupal\Core\Render\Renderer
   */
  protected $renderer;
  /**
   * Constructor.
   */
  public function __construct(Renderer $renderer) {
//..

Since it implements "Twig_ExtensionInterface" interface, we could be able to expose:

In this case, we go to expose a new  custom "Test" and "Function" functions by overwrite "getTest()" and "getFunctions()" methods, then we could check if a particular template already exists in current active theme and include it.

Inject Core Services

We will need "theme.manager" and "theme.registry" services, then we should use Dependency Injection to be able to use it within our "UtilityTwigExtension" class, let's modify "utility.service.yml",

services:
  utility.twig.extension:
    class: Drupal\utility\TwigExtension\UtilityTwigExtension
    arguments: ["@renderer", '@theme.manager', '@theme.registry']

We just added "@theme.registry" service as second argument and "@theme.registry" service as third argument to our custom service, then we should update our constructor as well,

// ...
  /**
   * Constructor.
   */
  public function __construct(Renderer $renderer, ThemeManager $theme_manager, Registry $theme_registry) {
    $this->renderer = $renderer;
    $this->themeManager = $theme_manager;
    $this->themeRegistry = $theme_registry;
// ...

Now we need to find a way to check all templates available on current active theme, it is possible that you are think in a bunch of ways to do that, but what if I told you that Drupal already does it for us, then we only need to take advance of that, then you might wondering about how?

Drupal has a function a lot of helpers functions, drupal_find_theme_templates is one of them, and it will discover any template inside given path, we will define a new property named: "$templates" to storage whatever that function returns, take a look how our constructor will looks like:

//..
  /**
   * @var \Drupal\Core\Theme\Registry
   */
  protected $themeRegistry;

  /**
   * @var \Drupal\Core\Theme\ThemeManager
   */
  protected $themeManager;

  /**
   * All theme templates from current theme.
   * @var array
   */
  protected $templates;


  /**
   * UtilityTwigExtension constructor.
   * @param \Drupal\Core\Render\Renderer $renderer
   * @param \Drupal\Core\Theme\ThemeManager $theme_manager
   * @param \Drupal\Core\Theme\Registry $theme_registry
   */
  public function __construct(Renderer $renderer, ThemeManager $theme_manager, Registry $theme_registry) {
    parent::__construct($renderer);
    $this->themeManager = $theme_manager;
    $this->themeRegistry = $theme_registry;
    $this->templates = drupal_find_theme_templates($this->themeRegistry->get(), '.html.twig', $this->themeManager->getActiveTheme()->getPath());
  }
//..

 

Create two helper methods

We will create two protected methods to use wide this custom TwigExtension, the firs one will help us to verify that given value is a string,

//..
  /**
   * Double check given value to be string.
   * @param $value string Given parameter to check
   * @return string
   *   String value.
   */
  protected function forceString($value) {
    // Value should be string value.
    return (is_string($value)) ? $value : '';
  }
//..

And next one will help us to build a proper machine name:

//..
  /**
   * Build template machine name based on template name.
   * @param $template string Given template name.
   * @return string
   *   Template machine name
   */
  protected function buildMachineName($template) {
    // Value should be string value.
    $template = $this->forceString($template);
    // Get template machine name.
    return str_replace('-', '_', $template);
  }
//..

We now are ready to rock!

Overwrite "getTest()" method

It is time to create a new test directive to be available in any Twig template, then we need to overwrite "getTest()" method by returning an array with an instance of "Twig_SimpleTest" class, where its constructor requests as first parameter a directive name and second parameter callable to be execute once this test is called from Twig template,

//..
  /**
   * {@inheritdoc}
   */
  public function getTests() {
    return [
      new \Twig_SimpleTest('ondisk', [$this, 'onDisk']),
    ];
  }
//..

In this case, we create a "ondisk" directive and setup callable a method inside current class named: "onDisk", then we need to implement that method that will return a boolean value,

//..
  /**
   * Verify if given template name exists on active theme.
   * @param $template String template name to check.
   * @return boolean
   *   TRUE when template already exists, otherwise false.
   */
  public function onDisk($template) {
    // Get template machine name.
    $key = $this->buildMachineName($template);
    // Check if current template exists.
    return array_key_exists($key, $this->templates);
  }
//..

 

Overwrite "getFunctions" method

We now go to implemented a function directive, we should return an array with an instance of "Twig_SimpleFunction" where its constructor requests as first parameter directive name and second parameter as callable to be invoke when it is called from Twig template,

//..
  /**
   * {@inheritdoc}
   */
  public function getFunctions() {
    return [
      new \Twig_SimpleFunction('template_path', [$this, 'templatePath']),
    ];
  }
//..

We are creating a new directive named: "template_path" and define "templatePath" method as callable, let's see how that method should looks like,

//..
  /**
   * Get path to given template name.
   * @param $template string Template name to check.
   * @return string
   *   Path to template, otherwise NULL.
   */
  public function templatePath($template){
    // Check that current template already exists.
    if($this->onDisk($template)) {
      // Get template machine name.
      $key = $this->buildMachineName($template);
      // Return path template.
      return base_path() . $this->templates[$key]['path'] . '/';
    }
    // If it fails then return NULL.
    return NULL;
  }
//..

Exposing our new TwigExtension

Firstly we need to implement "getName()" method to define an unique TwigExtension name, so it will looks like this,

//..
  /**
   * {@inheritdoc}
   */
  public function getName() {
    return 'utility.twig.extension';
  }
//..

At last, but not least, we need to tell to Drupal that there is a new TwigExtension, and to achieve it we need to add "twig.extension" tag to service file,

services:
  utility.twig.extension:
    class: Drupal\utility\TwigExtension\UtilityTwigExtension
    arguments: ["@renderer", '@theme.manager', '@theme.registry']
    tags:
      - { name: twig.extension }

By the way, you could find this custom module at github.

Usage a new directive

Let's modify our "page.html.twig" twig template, I highly recommend to have a custom theme where we could do it, I'll write an article about it, but not now. Then we go to make it directly into our active theme,

{# ... #}
<div id="page-wrapper">
  <div id="page">
    <header id="header" class="header" role="banner" aria-label="{{ 'Site header'|t }}">
      <div class="section layout-container clearfix">
        {% set template = 'utility-awesome-header' %}
        {% if template is ondisk %}
          {% include template_path(template) ~ template ~ '.html.twig' %}
        {% else %}
          {{ page.secondary_menu }}
          {{ page.header }}
          {{ page.primary_menu }}
        {% endif %}
{# ... #}

We only define a variable named: "template" with name partial template, then we check if that template already exists on current active theme and include that, we use our custom function named: "template_path" to retrieve correct path.

AED We learned

At the end of day, we learned how to create a custom TwigExtension and expose a new "Test" and "Function" directive, also we implemented into a Twig template, there're a lot of use cases you might imagine from here, then take advance of that,

See ya later alligator,