Defining new tokens

We just saw how we can programmatically use existing tokens inside our strings and get them replaced with minimal effort. All we need is the token service and the data object that can be used to replace the token. Keep in mind that there are tokens that don't even require any data objects due to their global nature. The hook_tokens() implementation will take care of that—let's see how.

In the previous chapter, we created functionalities for a dynamic Hello World message: either calculated on the fly or loaded from a configuration object. How about we expose that message as a token? This would make its usage more flexible because our string becomes exposed to the entire token system.

As mentioned earlier, we will start with the hook_token_info() implementation:

/**
* Implements hook_token_info().
*/
function hello_world_token_info() {
$type = [
'name' => t('Hello World'),
'description' => t('Tokens related to the Hello World module.'),
];

$tokens['salutation'] = [
'name' => t('Salutation'),
'description' => t('The Hello World salutation value.'),
];

return [
'types' => ['hello_world' => $type],
'tokens' => ['hello_world' => $tokens],
];
}

In here, we will need to define two things—the types and the tokens. In our case, we are defining one of each. The type is hello_world and comes with a human-readable name and description in case it needs to be rendered somewhere in the UI. The token is salutation and belongs to the hello_world type. It also gets a name and description. At the end, we return an array that contains both.

What follows is the hook_tokens() implementation in which we handle the replacement of our token:

/**
* Implements hook_tokens().
*/
function hello_world_tokens($type, $tokens, array $data, array $options, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata) {
$replacements = [];
if ($type == 'hello_world') {
foreach ($tokens as $name => $original) {
switch ($name) {
case 'salutation':
$replacements[$original] = \Drupal::service('hello_world.salutation')->getSalutation();
$config = \Drupal::config('hello_world.custom_salutation');
$bubbleable_metadata->addCacheableDependency($config);
break;

}
}
}

return $replacements;
}

There is a bit more going on here, but I'll explain everything. This hook gets fired whenever a replacement of tokens is attempted on a string. And it's fired for each type that has been found inside that string, $type being the first argument. Inside $tokens, we get an array of tokens located in that string, which belong to $type. The $data array contains the objects needed to replace the tokens (and passed to the replace() method), keyed by the type. This array can be empty (as it will be in our case).

Inside the function, we loop through each token of this group and try to replace it. We only know of one, and we use our HelloWorldSalutation service to determine the replacement string.

Finally, the function needs to return an array of all replacements found (which can be multiple if multiple tokens of the same type are found inside a string).

The bubbleable_metadata parameter is a special cache metadata object that describes this token in the cache system. It is needed because tokens get cached, so if any dependent object changes, the cache needs to be invalidated for this token as well. By default, all objects inside the $data array are read and included in this object. However, in our case, it is empty, yet we still depend on a configuration object that can change—the one that stores the overridden salutation message. So, we will need to add a dependency on that configuration object even if the actual value for the salutation we compute uses the same HelloWorldSalutation service we used before. So, we have a simple example here, but with a complex twist. We will talk more about caching later in the book.

That's all there is to defining our token. It can now also be used inside strings and replaced using the Token service. Something like this:

$final_string = \Drupal::token()->replace('The salutation text is: [hello_world:salutation]'); 

As you can see, we pass no other parameters. If our token was dependent on an entity object, for example, we would have passed it in the second parameter array and have made use of it inside hook_tokens() to compute the replacement.