Skip to main content

Creating your own tokens in a Drupal module

Back-end Development
Drupal

In previous articles, we discussed the Drupal token substitution system and how to run token replacement in your custom code.

To complete the picture, we need to look at how tokens are created in the first place. In this article, we'll build a few simple examples of token creation so that you can show your module's data to the world.

The hooks

Drupal's token system is still wedded to the venerable hook system for extending functionality. It relies on two central pieces: hook_token_info() and hook_tokens().

We first need to declare what our tokens will be. Assuming we have a module we called token_example:

/**
 * Implements hook_token_info().
 */
function token_example_token_info() {
  return [
    'types' => [
      'global-example' => [
        'name' => t('Global examples'),
        'description' => t('Demonstration of tokens with no context.'),
      ],
    'tokens' => [
      'global-example' => [
        'fruit' => [
          'name' => t('Fruit'),
        ],
      ],
    ],
  ];
}

This declares a new token type called global-example, within which is a token called fruit. The user will therefore access this token by typing [global-example:fruit].

The other hook lets us determine what the actual value of the token is when it's time to do the replacement.

/**
 * Implements hook_tokens().
 */
function token_example_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
  $replacements = [];

  switch ($type) {
    case 'global-example':
      foreach ($tokens as $key => $token) {
        switch ($key) {
          case 'fruit':
            $replacements[$token] = 'kiwi';
            break;
        }
      }
      break;
  }

  return $replacements;
}

We are using switch statements here, with the idea that this hook could handle a number of different tokens and token types. Obviously this could be a simple if instead, in this simple situation.

With the above hook in place, [global-example:fruit] will be replaced by kiwi whenever the ->replace() method is called.

Using context

To make our token more interesting, we need to make use of the data that the calling code can pass to us. You can see the $data parameter above, but in order to rely on it being full of useful information, we need to declare that our token consumes a particular kind of object.

/**
 * Implements hook_token_info().
 */
function token_example_token_info() {
  return [
    'types' => [
      'node-example' => [
        'name' => t('Node examples'),
        'description' => t('Demonstration of tokens with a node context.'),
        'needs-data' => 'node',
      ],
    ],
    'tokens' => [
      'node-example' => [
        'timestamps' => [
          'name' => t('Timestamps'),
        ],
      ],
    ],
  ];
}

We have defined a new type of token called node-example, and this time we provided the needs-data attribute. This will tell the token system not to bother with this kind of token unless the module that is running token substitution also passes along a node object for us.

Now we can use this data when we provide the replacement values.

/**
 * Implements hook_tokens().
 */
function token_example_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
  $replacements = [];

  switch ($type) {
    case 'node-example':
      foreach ($tokens as $key => $token) {
        if ($key == 'timestamps') {
          $replacements[$token] = $data['node']->created->value . ' and ' . $data['node']->changed->value;
        }
      }
      break;
  }

  return $replacements;
}

Since we requested a node object, we can rely on it being present in $data['node'] when we need it.

Chaining tokens together

For a final trick, we can make more sophisticated tokens that pass info to other tokens. This time we give the token a type when we define it.

/**
 * Implements hook_token_info().
 */
function token_example_token_info() {
  return [
    'types' => [
      'global-example' => [
        'name' => t('Global examples'),
        'description' => t('Demonstration of tokens with no context.'),
      ],
    ],
    'tokens' => [
      'global-example' => [
        'basic-node' => [
          'name' => t('Basic node'),
          'type' => 'node',
        ],
      ],
    ],
  ];
}

The type lets Drupal know what information the token can retrieve, and therefore which other tokens can chain on to it.

When we define the replacement values, we have to do just a little more work to support the chain.

/**
 * Implements hook_tokens().
 */
function token_example_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
  $replacements = [];

  switch ($type) {
    case 'global-example':
      foreach ($tokens as $key => $token) {
        switch ($key) {
          case 'basic-node':
            $replacements[$token] = Node::load(1)->title->value;
            break;
        }
      }

      $node_tokens = \Drupal::token()->findWithPrefix($tokens, 'basic-node');
      $replacements += \Drupal::token()->generate('node', $node_tokens, ['node' => Node::load(1)], $options, $bubbleable_metadata);
      break;
  }

  return $replacements;
}

You may notice we are defining replacements in two separate places here. The first one looks like we have seen previously. It tells the system that when it sees [global-example:basic-node] by itself, it should grab the title of node ID 1 and substitute it. This is the "default" value of the token without chaining.

The rest of the code handles the chaining. We use a couple handy utility methods from the token service for this. The first, ->findWithPrefix(), does the work of combing through the provided tokens for any that match the current pattern. Then the second, ->generate(), tells other token definitions that deal with node objects that they should do their own replacements, using the node that we provide to them.

All together now

Here are all of these examples combined into a single file:

<?php

/**
 * @file
 * Hooks to demonstrate creation of tokens.
 */

use Drupal\Core\Render\BubbleableMetadata;
use Drupal\node\Entity\Node;

/**
 * Implements hook_token_info().
 */
function token_example_token_info() {
  return [
    'types' => [
      'global-example' => [
        'name' => t('Global examples'),
        'description' => t('Demonstration of tokens with no context.'),
      ],
      'node-example' => [
        'name' => t('Node examples'),
        'description' => t('Demonstration of tokens with a node context.'),
        'needs-data' => 'node',
      ],
    ],
    'tokens' => [
      'global-example' => [
        'fruit' => [
          'name' => t('Fruit'),
        ],
        'basic-node' => [
          'name' => t('Basic node'),
          'type' => 'node',
        ],
      ],
      'node-example' => [
        'timestamps' => [
          'name' => t('Timestamps'),
        ],
      ],
    ],
  ];
}


/**
 * Implements hook_tokens().
 */
function token_example_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
  $replacements = [];

  switch ($type) {
    case 'global-example':
      foreach ($tokens as $key => $token) {
        switch ($key) {
          case 'fruit':
            $replacements[$token] = 'kiwi';
            break;
          case 'basic-node':
            $replacements[$token] = Node::load(1)->title->value;
            break;
        }
      }

      $node_tokens = \Drupal::token()->findWithPrefix($tokens, 'basic-node');
      $replacements += \Drupal::token()->generate('node', $node_tokens, ['node' => Node::load(1)], $options, $bubbleable_metadata);

      break;

    case 'node-example':
      foreach ($tokens as $key => $token) {
        if ($key == 'timestamps') {
          $replacements[$token] = $data['node']->created->value . ' and ' . $data['node']->changed->value;
        }
      }
      break;
  }

  return $replacements;
}

And that's it! Use these techniques to make your code more flexible and give more power to your site administrators.

Need a fresh perspective on a tough project?

Let’s talk about how RDG can help.

Contact Us