CREATING A CUSTOM TOKEN IN DRUPAL 8

Tokens represent an inline replacement when evaluating or rendering text. In Drupal, the Token module dates back to Drupal 4.xand still exists for Drupal 8 (and Drupal 9). This has long been the backbone of powerful features in Drupal, such as user-configurable dynamic aliases with Pathauto, robust meta tag control with Metatag, which allows editors to leverage tokens in text area/WYSIWYG fields via the token filter and many other modules.

Out of the box, there are many tokens to solve a variety of common use cases for them. Occasionally, you may encounter a requirement where no such token exists to facilitate it. Fortunately, implementing custom tokens in Drupal is fairly straightforward.

In a project I'm working on, we're migrating over 100,000 pieces of content from a legacy system. We've done our best to create URL alias patterns for each of the 10 content types, but there's one case that can't be done from the UI alone with standard tokens. With so much content from a legacy system, the chances of finding the same title more than once are high. What happens then, if you are familiar with Pathauto, is that in the case of an existing alias, it is inferred by adding a number to the end of it. So, if I had an alias of chapter one, and then migrated to another node Chapter also titled "Chapter one" (this happens a lot), then my path alias will end up being 'chapter-one-0' or 'chapter-one-1'.

This is a basic example and not a real title, but given your content architecture, some chapters are subchapters of a chapter, and all chapters are part of a book node type. Alias collisions occur quite a bit. The client expressed the ability to use a different field for the alias if it exists, rather than using the node title. They did not want numbers added to the URL.

There are a few ways this could have been solved. One way would have been to use hook_pathauto_pattern_alter to inspect and change the pattern used to create an alias when saving based on certain conditions. This is a valid approach. However, in the client context, I thought this behaviour would be too "magical" and perhaps not clear later as the editorial process begins to move from the old application to the new Drupal platform. Second, it would remove the ability to dictate the UI URL pattern. Third, providing a new token opens up some functionality to resolve the URL without sacrificing UI control, and makes that token usable in other contexts, e.g., disambiguating the search results title, which affects the meta tag output or other possible uses. Through the definition of the token, I can describe the editors so they know exactly what that token will do.

So we create that token. When used, the token will be replaced with one of two possible values:

  • The value in the "Chapter Display Title" field.
  • Fall back to the node title if this field has no value.

Token test

First, we can write a test for our token. Our module is called " mymodule_content_tokens ". "mymodule_content_tokens_test " is a module within that module that is used to provide a Chapter node type definition and a "Chapter display title" field definition. Enabling this ensures that the node type and field exist when the test is run. The test method creates two chapter nodes, one with a chapter display field value and one without, tests the token result for them and then removes the field value from the second node and tests this result again:

<?php
namespace Drupal\Tests\mymodule_content_tokens\Kernel\Utility;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\node\Entity\Node;
use Drupal\Core\Utility\Token;
/**
 * Class ContentTokensTest
 *
 * @group mymodule_content_tokens
 * @package Drupal\Tests\mymodule_content_tokens\Kernel\Utility
 */
class ContentTokensTest extends EntityKernelTestBase {
  use NodeCreationTrait;
  /**
   * @var \Drupal\Core\Utility\Token $tokenService
   */
  protected $tokenService;
  /**
   * @var array $modules
   */
  public static $modules = [
    'token',
    'node',
    'mymodule_content_tokens',
    'mymodule_content_tokens_test',
  ];
  /**
   * {@inheritdoc}
   */
  public function setUp() {
    parent::setUp();
    $this->installEntitySchema('node');
    $this->installConfig(['node']);
    $this->installSchema('node', ['node_access']);
    $this->installConfig(['mymodule_content_tokens_test']);
    $this->tokenService = $this->container->get('token');
  }
  /**
   * Tests that the chapter title token returns what we expect.
   */
  public function testChapterDisplayTitleTokenValue() {
    /** @var \Drupal\node\Entity\Node $node1 */
    $node1 = $this->createNode(['title' => 'Test Chapter One', 'type' => 'chapter']);
    /** @var \Drupal\node\Entity\Node $node2 */
    $node2 = $this->createNode(['title' => 'Test Chapter Two', 'type' => 'chapter', 'field_chapter_display_title' => 'Title Override']);
    $this->assertEqual('Test Chapter One', $this->tokenService->replace('[mymodule:chapter_display_title]', ['node' => $node1]));
    $this->assertEqual('Title Override', $this->tokenService->replace('[mymodule:chapter_display_title]', ['node' => $node2]));
    // Should revert back to node title when field_chapter_display_title is empty.
    $node2->field_chapter_display_title = '';
    $node2->save();
    $this->assertEqual('Test Chapter Two', $this->tokenService->replace('[mymodule:chapter_display_title]', ['node' => $node2]));
  }
}

Pretty simple, and provides a clear path for our implementation.

The token

To provide a new token(s), we need to use two hooks to tell Drupal about them: hook_token_info and hook_tokens . The info hook allows us to define one or more tokens, as well as a group to place them in:

<?php
declare(strict_types = 1);
use Drupal\Core\Render\BubbleableMetadata;
/**
 * Implements hook_token_info().
 */
function mymodule_content_tokens_token_info() : array {
  $info = [];
  $info['types']['mymodule'] = [
    'name' => t('MyModule Tokens'),
    'description' => t('Custom tokens to solve use-case problems for the our website.'),
  ];
  $info['tokens']['mymodule']['chapter_display_title'] = [
    'name' => 'Chapter Display Title',
    'description' => t('This token will return the chapter of a title either using the default node title, or the chapter_display_title field value on the Chapter. Useful for setting URL alias pattern.')
  ];
  return $info;
}

This will display the token everywhere on the system in the "MyModule Tokens" group type. You can use existing groups if you wish, such as Node. In my case, I wanted to separate them so they are easy to find. The token has a descriptive name and a description that says what it does, and that appears in the token browser.

The second hook, hook_tokens , tells Drupal what to replace our token with when it is found:

/**
 * Implements hook_tokens().
 */
function mymodule_content_tokens_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) : array {
  $replacements = [];
  if ($type == 'mymodule') {
    foreach ($tokens as $name => $original) {
      switch ($name) {
        case 'chapter_display_title':
          $node = $data['node'];
          if ($node->hasField('field_chapter_display_title') && !$node->get('field_chapter_display_title')->isEmpty()) {
            $text = $node->field_chapter_display_title->value;
          }
          $replacements[$original] = $text ?? $node->getTitle();
          break;
        default:
          break;
      }
    }
  }
  return $replacements;
}

We take the node's data context and check if this node object has the field (a simple check in case the token was mistakenly used for another type of node). If it does and has a value, then we use it. If not, it defaults to the title of the node that will always exist. The token will be replaced with the value assigned to $text. If the node is updated and that field is set to empty, then the token will simply return the node title. This makes the token useful for a site editor.

Now if we run our tests:

phpunit -- --group=mymodule_content_tokens
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.
Testing 
.                                                                   1 / 1 (100%)
Time: 20 seconds, Memory: 10.00MB
OK (1 test, 10 assertions)

Woohoo! We are now free to use our new token, and we have laid the groundwork for adding more tokens in the future if we need them. You may be wondering why we are only testing the token replacement result and not also verifying a generated alias. After all, that was our main requirement, right? Of course. This particular primary token usage will be in Pathauto. However, all we really want to test is that the token is replaced in the way we expect. As it is, we know Pathauto is going to work. We don't need to add tests and specifically alias generation test results.

Have Any Project in Mind?

If you want to do something in Drupal maybe you can hire me.

Either for consulting, development or maintenance of Drupal websites.