Last lesson you filled out the catalog card — hook_token_info() told Drupal that [example:username] and [example:user_email] exist and what they mean. But a catalog card doesn't fetch the product; it just describes it. hook_tokens() is the warehouse worker who actually goes and gets the real value the moment someone redeems that bracket-notation placeholder. This lesson wires that logic up for the example_token module from before.
What you'll learn in this lesson
- The exact job
hook_tokens()does, and how it differs fromhook_token_info() - How to read the
$typeand$tokensparameters Drupal hands you - Why the
BubbleableMetadataparameter matters even when you don't touch it directly - A common bug: forgetting to key your replacement array correctly
Implementing the replacement logic
/**
* Implements hook_tokens().
*/
function example_token_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
$replacements = [];
if ($type == 'example') {
$current_user = \Drupal::currentUser();
foreach ($tokens as $name => $original) {
switch ($name) {
case 'username':
$replacements[$original] = $current_user->getDisplayName();
break;
case 'user_email':
$replacements[$original] = $current_user->getEmail();
break;
}
}
}
return $replacements;
}
Drupal calls this hook once per token type actually present in the text being processed, not once per individual token — that's exactly why the first thing this function does is check if ($type == 'example') and bail out (implicitly, by returning an empty array) for every other type it doesn't own. Inside that check, $tokens is an associative array where the key is the short token name (username) and the value is the original full bracket string ([example:username]) — you must key $replacements by that original string, not the short name, or Drupal won't know which placeholder to swap it into.
The parameter you didn't touch: $bubbleable_metadata
This example ignores $bubbleable_metadata, and for a value as simple as the logged-in user's own name, that's fine. But if your replacement value depended on something cacheable — a specific node, a config object, anything with its own cache tags — you'd call $bubbleable_metadata->addCacheableDependency($entity) here. Skipping it when it matters is a subtle bug: the token still resolves correctly the first time, then serves a stale cached value forever after, because Drupal's render cache was never told this output depends on that entity.
$replacements by the short name ('username') instead of the original bracket string ($original, i.e. [example:username]). The function runs without error either way — nothing throws — but the token in your rendered output silently stays as literal, unreplaced text, which is a confusing thing to debug precisely because there's no error message pointing at the cause.Quick check: if a page uses both
[example:username]and[node:title]in the same text, does yourexample_token_tokens()implementation get called for both? (No — Drupal groups tokens by type before dispatching, so your hook only ever runs with$type == 'example'for theexample-type tokens;[node:title]is handled entirely by core's own node token hook, which your code never sees.)
Key takeaways
hook_tokens()is called once per token type present in the text, receiving every token of that type at once in$tokens.- Key the
$replacementsarray by the original bracket string ($original), never the short token name — this is the single most common mistake with this hook. - Always guard your logic with a
$typecheck; returning an empty array for types you don't own is correct, not an error. - Call
$bubbleable_metadata->addCacheableDependency()whenever your replacement value depends on something with its own cache lifecycle, or you risk serving stale data forever.
Coming up next
Your example tokens are now fully functional end to end — registered, discoverable, and correctly replaced at render time. Next, we step outside the Token API entirely and look at how a module reaches into Drupal's actual content: accessing entities programmatically, the foundation nearly every other API in this course builds on top of.