Prior to 8.0.0-beta13
Prior to 8.0.0-beta13, token replacement did not take into account cacheability metadata.
For example a custom controller could include a node token such as [node:title]
in its markup without the cacheability metadata for that markup/page taking into account the node that was passed, even though the resulting page really does depend on the cacheability of the node (if the node's title is modified, then the page needs to be invalidated to remain correct).
Therefore this would have lead to stale caches or even security issues.
hook_tokens()
implementations-
function mymodule_tokens($type, $tokens, array $data = array(), array $options = array()) { // ... case 'author': $account = $node->getOwner() ? $node->getOwner() : User::load(0); $replacements[$original] = $sanitize ? Html::escape($account->label()) : $account->label(); break; if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) { $replacements += $token_service->generate('user', $author_tokens, array('user' => $node->getOwner()), $options); } }
- Calls to
Token::replace()
-
class SomeController extends ControllerBase { function fooBar(NodeInterface $node) { $build['#markup'] = $this->token->replace('Tokens: [node:nid] [current-user:uid]', ['node' => $node]); // This render array doesn't convey that it should only be cached until $node is updated // and is only relevant for the current user. return $build; } }
After 8.0.0-beta13
Callers of \Drupal\Core\Utility\Token::replace()
should pass along a new Drupal\Core\Render\BubbleableMetadata
object. The Token API will set the required metadata on the passed object which can then be applied to the built render array.
(BubbleableMetadata
was chosen instead of CacheableMetadata
, because besides cacheability metadata, some token replacements may want to use placeholders, which requires the use of BubbleableMetadata
, so placeholders can bubble up.)
If you don't pass along a BubbleableMetadata
object to collect bubbleable metadata for the generated tokens, the bubbleable metadata will instead be automatically bubbled onto the render context (if the token replacement happens as part of rendering).
hook_tokens()
implementations-
use Drupal\Core\Render\BubbleableMetadata; function mymodule_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { case 'author': $account = $node->getOwner() ? $node->getOwner() : User::load(0); $bubbleable_metadata->addCacheableDependency($account); $replacements[$original] = $sanitize ? Html::escape($account->label()) : $account->label(); break; if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) { $replacements += $token_service->generate('user', $author_tokens, array('user' => $node->getOwner()), $options, $bubbleable_metadata); }
-
How tokens get the right bubbleable (cacheability + attachments, which includes placeholders) metadata:
- The associative array of data objects
$data
: data objects implementingCacheableDependencyInterface
orAttachmentsInterface
automatically have their bubbleable metadata (again: cacheability plus attachments, which includes placeholders) added to all tokens resulting from thathook_tokens()
implementation - "Related objects" tokens, such as
[node:author:*]
tokens, call the token service'sgenerate()
method with a new$data
array containing the related data objects, which also implementCacheableDependencyInterface
. In the example of author tokens:$node->getOwner()
returns aUser
entity, which also implementsCacheableDependencyInterface
. (See the above code sample.) - Finally, for global tokens, such as
[site:name]
, it is necessary to explicitly load the config object, and add it to$bubbleable_metadata
, for example:
switch ($name) { case 'name': $config = \Drupal::config('system.site'); $bubbleable_metadata->addCacheableDependency($config); $site_name = $config->get('name'); $replacements[$original] = $sanitize ? Html::escape($site_name) : $site_name; break;
- The associative array of data objects
- Calls to
Token::replace()
-
use Drupal\Core\Render\BubbleableMetadata; class SomeController extends ControllerBase { public function tokenReplace(NodeInterface $node) { // Create a BubbleableMetadata object to collect cacheability metadata. $bubbleable_metadata = new BubbleableMetadata(); // Pass it to Token::replace(). $build['#markup'] = $this->token->replace('Tokens: [node:nid] [current-user:uid]', ['node' => $node], [], $bubbleable_metadata); // At this point, $bubbleable_metadata contains the cache tags 'node:5' and 'user:3', // assuming node 5 was passed and the current user is user 3. // This merges those 'node:5' and 'user:3' cache tags onto the $build render array. $bubbleable_metadata->applyTo($build); return $build; } }