🔹 How to Create a Custom Decorator for Token in Drupal
In Drupal, tokens allow dynamic values to be substituted into text, email templates, fields, and more. There are two main Token classes:
| Class | Namespace | Source | Role |
|---|---|---|---|
Drupal\Core\Utility\Token | Core | Drupal core | Minimal implementation: basic replace() and hook_token_info() support. |
Drupal\token\Token | Contrib module Token | contrib | Extended functionality: token sorting, global token types, validation (getInvalidTokens*), caching, better API for UI. |
⚠️ Core remains lightweight, while the contrib Token module adds full features.
1️⃣ Why are there two Token classes?
Drupal Core provides minimal functionality to avoid adding extra complexity.
The Token module extends the core class by adding:
- sorting tokens by name,
- global tokens that are always valid,
- token validation in different contexts,
- caching and UI support.
Which Token class is used depends on the installed modules:
$token_service = \Drupal::service('token');
dump(get_class($token_service));
// Outputs: Drupal\Core\Utility\Token or Drupal\token\Token
2️⃣ Why create a custom decorator?
You might want to:
- log all
replace()calls, - filter or block specific tokens,
- add custom prefixes or dynamic tokens,
- implement a "dry-run" mode (scan tokens without replacing them).
A service decorator over Token is the best approach.
3️⃣ How to create a Token decorator
Step 1. Register the service
In mymodule.services.yml:
services:
mymodule.token_decorator:
decorates: token
class: Drupal\mymodule\TokenDecorator
arguments: ['@mymodule.token_decorator.inner']
⚡ decorates wraps the existing service without changing core or contrib.
Step 2. Create the TokenDecorator class
File: src/TokenDecorator.php
<?php
namespace Drupal\mymodule;
use Drupal\token\Token as ContribToken;
/
* Decorator for the token service.
*/
class TokenDecorator extends ContribToken {
/
* @var \Drupal\token\Token
*/
protected $inner;
public function __construct(ContribToken $inner) {
$this->inner = $inner;
}
/
* Extend replace() to add custom logic.
*/
public function replace($markup, array $data = [], array $options = [], $bubbleable_metadata = NULL) {
// 🔹 Log before replacement
\Drupal::logger('mymodule')->debug('Before replace: @markup', ['@markup' => $markup]);
// Use the original Token service
$result = $this->inner->replace($markup, $data, $options, $bubbleable_metadata);
// 🔹 Log after replacement
\Drupal::logger('mymodule')->debug('After replace: @result', ['@result' => $result]);
// 🔹 Custom filter: block specific tokens
$result = str_replace('[bad:token]', '[REMOVED]', $result);
return $result;
}
/
* Example: return all tokens for a specific type.
*/
public function getTokenVariants($type) {
$info = $this->inner->getInfo();
if (empty($info['tokens'][$type])) {
return [];
}
$variants = [];
foreach ($info['tokens'][$type] as $token => $data) {
$variants[$token] = $data['name'] ?? '';
}
return $variants;
}
}
4️⃣ What this decorator gives you
| Feature | Implemented? |
|---|---|
| Preserves base Token API behavior | ✅ |
| Logging tokens before/after replace() | ✅ |
| Custom token filtering | ✅ |
Extendable getInfo(), scan(), getInvalidTokens() | ✅ |
Globally overrides token service | ✅ |
5️⃣ Possible improvements
- Custom prefixes:
[env:...],[custom:...] - Role-based blocking: e.g., anonymous users cannot see
[user:email] - Dry-run mode: scan tokens without replacing
- JSON API for UI: provide all tokens and descriptions for autocomplete
6️⃣ Testing it
$token_service = \Drupal::service('token');
$markup = "Hello [node:title], bad token: [bad:token]";
echo $token_service->replace($markup, ['node' => $node]);
Log output:
Before replace: Hello [node:title], bad token: [bad:token]
After replace: Hello Example node title, bad token: [REMOVED]
✅ This gives a fully working custom Token decorator in Drupal.
Comments