5 * Token callbacks for the token module.
8 use Drupal\Core\Entity\ContentEntityInterface;
9 use Drupal\Core\Entity\FieldableEntityInterface;
10 use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
11 use Drupal\Core\Render\BubbleableMetadata;
12 use Drupal\Core\Render\Element;
13 use Drupal\Component\Utility\Crypt;
14 use Drupal\Component\Utility\Html;
15 use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
17 use Drupal\field\FieldStorageConfigInterface;
18 use Drupal\menu_link_content\MenuLinkContentInterface;
19 use Drupal\node\Entity\Node;
20 use Drupal\node\Entity\NodeType;
21 use Drupal\node\NodeInterface;
22 use Drupal\system\Entity\Menu;
23 use Drupal\user\UserInterface;
24 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
25 use Drupal\Core\TypedData\PrimitiveInterface;
26 use Drupal\Core\Field\FieldStorageDefinitionInterface;
27 use Drupal\Core\Entity\ContentEntityTypeInterface;
28 use Drupal\image\Entity\ImageStyle;
31 * Implements hook_token_info_alter().
33 function token_token_info_alter(&$info) {
34 // Force 'date' type tokens to require input and add a 'current-date' type.
35 // @todo Remove when http://drupal.org/node/943028 is fixed.
36 $info['types']['date']['needs-data'] = 'date';
37 $info['types']['current-date'] = [
38 'name' => t('Current date'),
39 'description' => t('Tokens related to the current date and time.'),
43 // Add a 'dynamic' key to any tokens that have chained but dynamic tokens.
44 $info['tokens']['date']['custom']['dynamic'] = TRUE;
46 // The [file:size] may not always return in kilobytes.
47 // @todo Remove when http://drupal.org/node/1193044 is fixed.
48 if (!empty($info['tokens']['file']['size'])) {
49 $info['tokens']['file']['size']['description'] = t('The size of the file.');
52 // Remove deprecated tokens from being listed.
53 unset($info['tokens']['node']['tnid']);
54 unset($info['tokens']['node']['type']);
55 unset($info['tokens']['node']['type-name']);
57 // Support 'url' type tokens for core tokens.
58 if (isset($info['tokens']['comment']['url']) && \Drupal::moduleHandler()->moduleExists('comment')) {
59 $info['tokens']['comment']['url']['type'] = 'url';
61 if (isset($info['tokens']['node']['url']) && \Drupal::moduleHandler()->moduleExists('node')) {
62 $info['tokens']['node']['url']['type'] = 'url';
64 if (isset($info['tokens']['term']['url']) && \Drupal::moduleHandler()->moduleExists('taxonomy')) {
65 $info['tokens']['term']['url']['type'] = 'url';
67 $info['tokens']['user']['url']['type'] = 'url';
69 // Add [token:url] tokens for any URI-able entities.
70 $entities = \Drupal::entityTypeManager()->getDefinitions();
71 foreach ($entities as $entity => $entity_info) {
72 // Do not generate tokens if the entity doesn't define a token type or is
73 // not a content entity.
74 if (!$entity_info->get('token_type') || (!$entity_info instanceof ContentEntityTypeInterface)) {
78 $token_type = $entity_info->get('token_type');
79 if (!isset($info['types'][$token_type]) || !isset($info['tokens'][$token_type])) {
80 // Define tokens for entity type's without their own integration.
81 $info['types'][$entity_info->id()] = [
82 'name' => $entity_info->getLabel(),
83 'needs-data' => $entity_info->id(),
88 // Add [entity:url] tokens if they do not already exist.
89 // @todo Support entity:label
90 if (!isset($info['tokens'][$token_type]['url'])) {
91 $info['tokens'][$token_type]['url'] = [
93 'description' => t('The URL of the @entity.', ['@entity' => mb_strtolower($entity_info->getLabel())]),
99 // Add [entity:original] tokens if they do not already exist.
100 if (!isset($info['tokens'][$token_type]['original'])) {
101 $info['tokens'][$token_type]['original'] = [
102 'name' => t('Original @entity', ['@entity' => mb_strtolower($entity_info->getLabel())]),
103 'description' => t('The original @entity data if the @entity is being updated or saved.', ['@entity' => mb_strtolower($entity_info->getLabel())]),
105 'type' => $token_type,
110 // Add support for custom date formats.
111 // @todo Remove when http://drupal.org/node/1173706 is fixed.
112 $date_format_types = \Drupal::entityTypeManager()->getStorage('date_format')->loadMultiple();
113 foreach ($date_format_types as $date_format_type => $date_format_type_info) {
114 /* @var \Drupal\system\Entity\DateFormat $date_format_type_info */
115 if (!isset($info['tokens']['date'][$date_format_type])) {
116 $info['tokens']['date'][$date_format_type] = [
117 'name' => Html::escape($date_format_type_info->label()),
118 'description' => t("A date in '@type' format. (%date)", ['@type' => $date_format_type, '%date' => \Drupal::service('date.formatter')->format(\Drupal::time()->getRequestTime(), $date_format_type)]),
126 * Implements hook_token_info().
128 function token_token_info() {
130 $info['tokens']['node']['source'] = [
131 'name' => t('Translation source node'),
132 'description' => t("The source node for this current node's translation set."),
135 $info['tokens']['node']['log'] = [
136 'name' => t('Revision log message'),
137 'description' => t('The explanation of the most recent changes made to the node.'),
139 $info['tokens']['node']['content-type'] = [
140 'name' => t('Content type'),
141 'description' => t('The content type of the node.'),
142 'type' => 'content-type',
145 // Content type tokens.
146 $info['types']['content-type'] = [
147 'name' => t('Content types'),
148 'description' => t('Tokens related to content types.'),
149 'needs-data' => 'node_type',
151 $info['tokens']['content-type']['name'] = [
153 'description' => t('The name of the content type.'),
155 $info['tokens']['content-type']['machine-name'] = [
156 'name' => t('Machine-readable name'),
157 'description' => t('The unique machine-readable name of the content type.'),
159 $info['tokens']['content-type']['description'] = [
160 'name' => t('Description'),
161 'description' => t('The optional description of the content type.'),
163 $info['tokens']['content-type']['node-count'] = [
164 'name' => t('Node count'),
165 'description' => t('The number of nodes belonging to the content type.'),
167 $info['tokens']['content-type']['edit-url'] = [
168 'name' => t('Edit URL'),
169 'description' => t("The URL of the content type's edit page."),
173 // Taxonomy term and vocabulary tokens.
174 if (\Drupal::moduleHandler()->moduleExists('taxonomy')) {
175 $info['tokens']['term']['edit-url'] = [
176 'name' => t('Edit URL'),
177 'description' => t("The URL of the taxonomy term's edit page."),
180 $info['tokens']['term']['parents'] = [
181 'name' => t('Parents'),
182 'description' => t("An array of all the term's parents, starting with the root."),
185 $info['tokens']['term']['root'] = [
186 'name' => t('Root term'),
187 'description' => t("The root term of the taxonomy term."),
191 $info['tokens']['vocabulary']['machine-name'] = [
192 'name' => t('Machine-readable name'),
193 'description' => t('The unique machine-readable name of the vocabulary.'),
195 $info['tokens']['vocabulary']['edit-url'] = [
196 'name' => t('Edit URL'),
197 'description' => t("The URL of the vocabulary's edit page."),
203 $info['tokens']['file']['basename'] = [
204 'name' => t('Base name'),
205 'description' => t('The base name of the file.'),
207 $info['tokens']['file']['extension'] = [
208 'name' => t('Extension'),
209 'description' => t('The extension of the file.'),
211 $info['tokens']['file']['size-raw'] = [
212 'name' => t('File byte size'),
213 'description' => t('The size of the file, in bytes.'),
217 // Add information on the restricted user tokens.
218 $info['tokens']['user']['cancel-url'] = [
219 'name' => t('Account cancellation URL'),
220 'description' => t('The URL of the confirm delete page for the user account.'),
221 'restricted' => TRUE,
224 $info['tokens']['user']['one-time-login-url'] = [
225 'name' => t('One-time login URL'),
226 'description' => t('The URL of the one-time login page for the user account.'),
227 'restricted' => TRUE,
230 $info['tokens']['user']['roles'] = [
231 'name' => t('Roles'),
232 'description' => t('The user roles associated with the user account.'),
236 // Current user tokens.
237 $info['tokens']['current-user']['ip-address'] = [
238 'name' => t('IP address'),
239 'description' => t('The IP address of the current user.'),
242 // Menu link tokens (work regardless if menu module is enabled or not).
243 $info['types']['menu-link'] = [
244 'name' => t('Menu links'),
245 'description' => t('Tokens related to menu links.'),
246 'needs-data' => 'menu-link',
248 $info['tokens']['menu-link']['mlid'] = [
249 'name' => t('Link ID'),
250 'description' => t('The unique ID of the menu link.'),
252 $info['tokens']['menu-link']['title'] = [
253 'name' => t('Title'),
254 'description' => t('The title of the menu link.'),
256 $info['tokens']['menu-link']['url'] = [
258 'description' => t('The URL of the menu link.'),
261 $info['tokens']['menu-link']['parent'] = [
262 'name' => t('Parent'),
263 'description' => t("The menu link's parent."),
264 'type' => 'menu-link',
266 $info['tokens']['menu-link']['parents'] = [
267 'name' => t('Parents'),
268 'description' => t("An array of all the menu link's parents, starting with the root."),
271 $info['tokens']['menu-link']['root'] = [
273 'description' => t("The menu link's root."),
274 'type' => 'menu-link',
277 // Current page tokens.
278 $info['types']['current-page'] = [
279 'name' => t('Current page'),
280 'description' => t('Tokens related to the current page request.'),
282 $info['tokens']['current-page']['title'] = [
283 'name' => t('Title'),
284 'description' => t('The title of the current page.'),
286 $info['tokens']['current-page']['url'] = [
288 'description' => t('The URL of the current page.'),
291 $info['tokens']['current-page']['page-number'] = [
292 'name' => t('Page number'),
293 'description' => t('The page number of the current page when viewing paged lists.'),
295 $info['tokens']['current-page']['query'] = [
296 'name' => t('Query string value'),
297 'description' => t('The value of a specific query string field of the current page.'),
302 $info['types']['url'] = [
304 'description' => t('Tokens related to URLs.'),
305 'needs-data' => 'path',
307 $info['tokens']['url']['path'] = [
309 'description' => t('The path component of the URL.'),
311 $info['tokens']['url']['relative'] = [
312 'name' => t('Relative URL'),
313 'description' => t('The relative URL.'),
315 $info['tokens']['url']['absolute'] = [
316 'name' => t('Absolute URL'),
317 'description' => t('The absolute URL.'),
319 $info['tokens']['url']['brief'] = [
320 'name' => t('Brief URL'),
321 'description' => t('The URL without the protocol and trailing backslash.'),
323 $info['tokens']['url']['unaliased'] = [
324 'name' => t('Unaliased URL'),
325 'description' => t('The unaliased URL.'),
328 $info['tokens']['url']['args'] = [
329 'name' => t('Arguments'),
330 'description' => t("The specific argument of the current page (e.g. 'arg:1' on the page 'node/1' returns '1')."),
335 $info['types']['array'] = [
336 'name' => t('Array'),
337 'description' => t('Tokens related to arrays of strings.'),
338 'needs-data' => 'array',
341 $info['tokens']['array']['first'] = [
342 'name' => t('First'),
343 'description' => t('The first element of the array.'),
345 $info['tokens']['array']['last'] = [
347 'description' => t('The last element of the array.'),
349 $info['tokens']['array']['count'] = [
350 'name' => t('Count'),
351 'description' => t('The number of elements in the array.'),
353 $info['tokens']['array']['reversed'] = [
354 'name' => t('Reversed'),
355 'description' => t('The array reversed.'),
358 $info['tokens']['array']['keys'] = [
360 'description' => t('The array of keys of the array.'),
363 $info['tokens']['array']['join'] = [
364 'name' => t('Imploded'),
365 'description' => t('The values of the array joined together with a custom string in-between each value.'),
368 $info['tokens']['array']['value'] = [
369 'name' => t('Value'),
370 'description' => t('The specific value of the array.'),
375 $info['types']['random'] = [
376 'name' => t('Random'),
377 'description' => t('Tokens related to random data.'),
379 $info['tokens']['random']['number'] = [
380 'name' => t('Number'),
381 'description' => t('A random number from 0 to @max.', ['@max' => mt_getrandmax()]),
383 $info['tokens']['random']['hash'] = [
385 'description' => t('A random hash. The possible hashing algorithms are: @hash-algos.', ['@hash-algos' => implode(', ', hash_algos())]),
389 // Define image_with_image_style token type.
390 if (\Drupal::moduleHandler()->moduleExists('image')) {
391 $info['types']['image_with_image_style'] = [
392 'name' => t('Image with image style'),
393 'needs-data' => 'image_with_image_style',
398 // Provide tokens for the ImageStyle attributes.
399 $info['tokens']['image_with_image_style']['mimetype'] = [
400 'name' => t('MIME type'),
401 'description' => t('The MIME type (image/png, image/bmp, etc.) of the image.'),
403 $info['tokens']['image_with_image_style']['filesize'] = [
404 'name' => t('File size'),
405 'description' => t('The file size of the image.'),
407 $info['tokens']['image_with_image_style']['height'] = [
408 'name' => t('Height'),
409 'description' => t('The height the image, in pixels.'),
411 $info['tokens']['image_with_image_style']['width'] = [
412 'name' => t('Width'),
413 'description' => t('The width of the image, in pixels.'),
415 $info['tokens']['image_with_image_style']['uri'] = [
417 'description' => t('The URI to the image.'),
419 $info['tokens']['image_with_image_style']['url'] = [
421 'description' => t('The URL to the image.'),
429 * Implements hook_tokens().
431 function token_tokens($type, array $tokens, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata) {
433 $language_manager = \Drupal::languageManager();
434 $url_options = ['absolute' => TRUE];
435 if (isset($options['langcode'])) {
436 $url_options['language'] = $language_manager->getLanguage($options['langcode']);
437 $langcode = $options['langcode'];
440 $langcode = $language_manager->getCurrentLanguage()->getId();
444 if ($type == 'date') {
445 $date = !empty($data['date']) ? $data['date'] : \Drupal::time()->getRequestTime();
447 // @todo Remove when http://drupal.org/node/1173706 is fixed.
448 $date_format_types = \Drupal::entityTypeManager()->getStorage('date_format')->loadMultiple();
449 foreach ($tokens as $name => $original) {
450 if (isset($date_format_types[$name]) && _token_module('date', $name) == 'token') {
451 $replacements[$original] = \Drupal::service('date.formatter')->format($date, $name, '', NULL, $langcode);
456 // Current date tokens.
457 // @todo Remove when http://drupal.org/node/943028 is fixed.
458 if ($type == 'current-date') {
459 $replacements += \Drupal::token()->generate('date', $tokens, ['date' => \Drupal::time()->getRequestTime()], $options, $bubbleable_metadata);
463 if ($type == 'comment' && !empty($data['comment'])) {
464 /* @var \Drupal\comment\CommentInterface $comment */
465 $comment = $data['comment'];
467 // Chained token relationships.
468 if (($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url'))) {
469 // Add fragment to url options.
470 $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $comment->toUrl('canonical', ['fragment' => "comment-{$comment->id()}"])], $options, $bubbleable_metadata);
475 if ($type == 'node' && !empty($data['node'])) {
476 /* @var \Drupal\node\NodeInterface $node */
477 $node = $data['node'];
479 foreach ($tokens as $name => $original) {
482 $replacements[$original] = (string) $node->revision_log->value;
486 $type_name = \Drupal::entityTypeManager()->getStorage('node_type')->load($node->getType())->label();
487 $replacements[$original] = $type_name;
492 // Chained token relationships.
493 if (($parent_tokens = \Drupal::token()->findWithPrefix($tokens, 'source')) && $source_node = $node->getUntranslated()) {
494 $replacements += \Drupal::token()->generate('node', $parent_tokens, ['node' => $source_node], $options, $bubbleable_metadata);
496 if (($node_type_tokens = \Drupal::token()->findWithPrefix($tokens, 'content-type')) && $node_type = NodeType::load($node->bundle())) {
497 $replacements += \Drupal::token()->generate('content-type', $node_type_tokens, ['node_type' => $node_type], $options, $bubbleable_metadata);
499 if (($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url'))) {
500 $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $node->toUrl()], $options, $bubbleable_metadata);
504 // Content type tokens.
505 if ($type == 'content-type' && !empty($data['node_type'])) {
506 /* @var \Drupal\node\NodeTypeInterface $node_type */
507 $node_type = $data['node_type'];
509 foreach ($tokens as $name => $original) {
512 $replacements[$original] = $node_type->label();
516 $replacements[$original] = $node_type->id();
520 $replacements[$original] = $node_type->getDescription();
524 $count = \Drupal::entityQueryAggregate('node')
525 ->aggregate('nid', 'COUNT')
526 ->condition('type', $node_type->id())
528 $replacements[$original] = (int) $count;
532 $replacements[$original] = $node_type->toUrl('edit-form', $url_options)->toString();
538 // Taxonomy term tokens.
539 if ($type == 'term' && !empty($data['term'])) {
540 /* @var \Drupal\taxonomy\TermInterface $term */
541 $term = $data['term'];
543 /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
544 $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
546 foreach ($tokens as $name => $original) {
549 $replacements[$original] = Url::fromRoute('entity.taxonomy_term.edit_form', ['taxonomy_term' => $term->id()], $url_options)->toString();
553 if ($parents = token_taxonomy_term_load_all_parents($term->id(), $langcode)) {
554 $replacements[$original] = token_render_array($parents, $options);
559 $parents = $term_storage->loadAllParents($term->id());
560 $root_term = end($parents);
561 if ($root_term->id() != $term->id()) {
562 $root_term = \Drupal::service('entity.repository')->getTranslationFromContext($root_term, $langcode);
563 $replacements[$original] = $root_term->label();
569 // Chained token relationships.
570 if (($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url'))) {
571 $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $term->toUrl()], $options, $bubbleable_metadata);
573 // [term:parents:*] chained tokens.
574 if ($parents_tokens = \Drupal::token()->findWithPrefix($tokens, 'parents')) {
575 if ($parents = token_taxonomy_term_load_all_parents($term->id(), $langcode)) {
576 $replacements += \Drupal::token()->generate('array', $parents_tokens, ['array' => $parents], $options, $bubbleable_metadata);
579 if ($root_tokens = \Drupal::token()->findWithPrefix($tokens, 'root')) {
580 $parents = $term_storage->loadAllParents($term->id());
581 $root_term = end($parents);
582 if ($root_term->tid != $term->id()) {
583 $replacements += \Drupal::token()->generate('term', $root_tokens, ['term' => $root_term], $options, $bubbleable_metadata);
588 // Vocabulary tokens.
589 if ($type == 'vocabulary' && !empty($data['vocabulary'])) {
590 $vocabulary = $data['vocabulary'];
592 foreach ($tokens as $name => $original) {
595 $replacements[$original] = $vocabulary->id();
599 $replacements[$original] = Url::fromRoute('entity.taxonomy_vocabulary.edit_form', ['taxonomy_vocabulary' => $vocabulary->id()], $url_options)->toString();
606 if ($type == 'file' && !empty($data['file'])) {
607 $file = $data['file'];
609 foreach ($tokens as $name => $original) {
612 $basename = pathinfo($file->uri->value, PATHINFO_BASENAME);
613 $replacements[$original] = $basename;
617 $extension = pathinfo($file->uri->value, PATHINFO_EXTENSION);
618 $replacements[$original] = $extension;
622 $replacements[$original] = (int) $file->filesize->value;
629 if ($type == 'user' && !empty($data['user'])) {
630 /* @var \Drupal\user\UserInterface $account */
631 $account = $data['user'];
633 foreach ($tokens as $name => $original) {
636 if ($account instanceof UserInterface && $account->hasField('user_picture')) {
637 /** @var \Drupal\Core\Render\RendererInterface $renderer */
638 $renderer = \Drupal::service('renderer');
640 '#theme' => 'user_picture',
641 '#account' => $account,
643 $replacements[$original] = $renderer->renderPlain($output);
648 $roles = $account->getRoles();
649 $roles_names = array_combine($roles, $roles);
650 $replacements[$original] = token_render_array($roles_names, $options);
655 // Chained token relationships.
656 if ($account instanceof UserInterface && $account->hasField('user_picture') && ($picture_tokens = \Drupal::token()->findWithPrefix($tokens, 'picture'))) {
657 $replacements += \Drupal::token()->generate('file', $picture_tokens, ['file' => $account->user_picture->entity], $options, $bubbleable_metadata);
659 if ($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url')) {
660 $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $account->toUrl()], $options, $bubbleable_metadata);
662 if ($role_tokens = \Drupal::token()->findWithPrefix($tokens, 'roles')) {
663 $roles = $account->getRoles();
664 $roles_names = array_combine($roles, $roles);
665 $replacements += \Drupal::token()->generate('array', $role_tokens, ['array' => $roles_names], $options, $bubbleable_metadata);
669 // Current user tokens.
670 if ($type == 'current-user') {
671 foreach ($tokens as $name => $original) {
674 $ip = \Drupal::request()->getClientIp();
675 $replacements[$original] = $ip;
682 if ($type == 'menu-link' && !empty($data['menu-link'])) {
683 /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
684 $link = $data['menu-link'];
685 /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
686 $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
688 if ($link instanceof MenuLinkContentInterface) {
689 $link = $menu_link_manager->createInstance($link->getPluginId());
692 foreach ($tokens as $name => $original) {
695 $replacements[$original] = $link->getPluginId();
699 $replacements[$original] = token_menu_link_translated_title($link, $langcode);
703 $replacements[$original] = $link->getUrlObject()->setAbsolute()->toString();
707 /** @var \Drupal\Core\Menu\MenuLinkInterface $parent */
708 if ($link->getParent() && $parent = $menu_link_manager->createInstance($link->getParent())) {
709 $replacements[$original] = token_menu_link_translated_title($parent, $langcode);
714 if ($parents = token_menu_link_load_all_parents($link->getPluginId(), $langcode)) {
715 $replacements[$original] = token_render_array($parents, $options);
720 if ($link->getParent() && $parent_ids = array_keys(token_menu_link_load_all_parents($link->getPluginId(), $langcode))) {
721 $root = $menu_link_manager->createInstance(array_shift($parent_ids));
722 $replacements[$original] = token_menu_link_translated_title($root, $langcode);
728 // Chained token relationships.
729 /** @var \Drupal\Core\Menu\MenuLinkInterface $parent */
730 if ($link->getParent() && ($parent_tokens = \Drupal::token()->findWithPrefix($tokens, 'parent')) && $parent = $menu_link_manager->createInstance($link->getParent())) {
731 $replacements += \Drupal::token()->generate('menu-link', $parent_tokens, ['menu-link' => $parent], $options, $bubbleable_metadata);
733 // [menu-link:parents:*] chained tokens.
734 if ($parents_tokens = \Drupal::token()->findWithPrefix($tokens, 'parents')) {
735 if ($parents = token_menu_link_load_all_parents($link->getPluginId(), $langcode)) {
736 $replacements += \Drupal::token()->generate('array', $parents_tokens, ['array' => $parents], $options, $bubbleable_metadata);
739 if (($root_tokens = \Drupal::token()->findWithPrefix($tokens, 'root')) && $link->getParent() && $parent_ids = array_keys(token_menu_link_load_all_parents($link->getPluginId(), $langcode))) {
740 $root = $menu_link_manager->createInstance(array_shift($parent_ids));
741 $replacements += \Drupal::token()->generate('menu-link', $root_tokens, ['menu-link' => $root], $options, $bubbleable_metadata);
743 if ($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url')) {
744 $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $link->getUrlObject()], $options, $bubbleable_metadata);
749 // Current page tokens.
750 if ($type == 'current-page') {
751 $request = \Drupal::request();
752 foreach ($tokens as $name => $original) {
755 $route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT);
757 $title = \Drupal::service('title_resolver')->getTitle($request, $route);
758 $replacements[$original] = token_render_array_value($title);
763 $bubbleable_metadata->addCacheContexts(['url.path']);
765 $url = Url::createFromRequest($request)->setOptions($url_options);
767 catch (\Exception $e) {
768 // Url::createFromRequest() can fail, e.g. on 404 pages.
769 // Fall back and try again with Url::fromUserInput().
771 $url = Url::fromUserInput($request->getPathInfo(), $url_options);
773 catch (\Exception $e) {
774 // Instantiation would fail again on malformed urls.
778 $replacements[$original] = $url->toString();
783 if ($page = $request->query->get('page')) {
784 // @see PagerDefault::execute()
785 $pager_page_array = explode(',', $page);
786 $page = $pager_page_array[0];
788 $replacements[$original] = (int) $page + 1;
794 // [current-page:arg] dynamic tokens.
795 if ($arg_tokens = \Drupal::token()->findWithPrefix($tokens, 'arg')) {
796 $path = ltrim(\Drupal::service('path.current')->getPath(), '/');
797 // Make sure its a system path.
798 $path = \Drupal::service('path.alias_manager')->getPathByAlias($path);
799 foreach ($arg_tokens as $name => $original) {
800 $parts = explode('/', $path);
801 if (is_numeric($name) && isset($parts[$name])) {
802 $replacements[$original] = $parts[$name];
807 // [current-page:query] dynamic tokens.
808 if ($query_tokens = \Drupal::token()->findWithPrefix($tokens, 'query')) {
809 $bubbleable_metadata->addCacheContexts(['url.query_args']);
810 foreach ($query_tokens as $name => $original) {
811 if (\Drupal::request()->query->has($name)) {
812 $value = \Drupal::request()->query->get($name);
813 $replacements[$original] = $value;
818 // Chained token relationships.
819 if ($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url')) {
822 $url = Url::createFromRequest($request)->setOptions($url_options);
824 catch (\Exception $e) {
825 // Url::createFromRequest() can fail, e.g. on 404 pages.
826 // Fall back and try again with Url::fromUserInput().
828 $url = Url::fromUserInput($request->getPathInfo(), $url_options);
830 catch (\Exception $e) {
831 // Instantiation would fail again on malformed urls.
834 // Add cache contexts to ensure this token functions on a per-path basis
835 $bubbleable_metadata->addCacheContexts(['url.path']);
836 $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $url], $options, $bubbleable_metadata);
841 if ($type == 'url' && !empty($data['url'])) {
842 /** @var \Drupal\Core\Url $url */
844 // To retrieve the correct path, modify a copy of the Url object.
845 $path_url = clone $url;
847 // Ensure the URL is routed to avoid throwing an exception.
848 if ($url->isRouted()) {
849 $path .= $path_url->setAbsolute(FALSE)->setOption('fragment', NULL)->getInternalPath();
852 foreach ($tokens as $name => $original) {
855 $value = !($url->getOption('alias')) ? \Drupal::service('path.alias_manager')->getAliasByPath($path, $langcode) : $path;
856 $replacements[$original] = $value;
861 $alias = \Drupal::service('path.alias_manager')->getAliasByPath($path, $langcode);
862 $replacements[$original] = $alias;
866 $replacements[$original] = $url->setAbsolute()->toString();
870 $replacements[$original] = $url->setAbsolute(FALSE)->toString();
874 $replacements[$original] = preg_replace(['!^https?://!', '!/$!'], '', $url->setAbsolute()->toString());
878 $unaliased = clone $url;
879 $replacements[$original] = $unaliased->setAbsolute()->setOption('alias', TRUE)->toString();
883 $value = !($url->getOption('alias')) ? \Drupal::service('path.alias_manager')->getAliasByPath($path, $langcode) : $path;
884 $replacements[$original] = token_render_array(explode('/', $value), $options);
889 // [url:args:*] chained tokens.
890 if ($arg_tokens = \Drupal::token()->findWithPrefix($tokens, 'args')) {
891 $value = !($url->getOption('alias')) ? \Drupal::service('path.alias_manager')->getAliasByPath($path, $langcode) : $path;
892 $replacements += \Drupal::token()->generate('array', $arg_tokens, ['array' => explode('/', ltrim($value, '/'))], $options, $bubbleable_metadata);
895 // [url:unaliased:*] chained tokens.
896 if ($unaliased_tokens = \Drupal::token()->findWithPrefix($tokens, 'unaliased')) {
897 $url->setOption('alias', TRUE);
898 $replacements += \Drupal::token()->generate('url', $unaliased_tokens, ['url' => $url], $options, $bubbleable_metadata);
903 if (!empty($data[$type]) && $entity_type = \Drupal::service('token.entity_mapper')->getEntityTypeForTokenType($type)) {
904 /* @var \Drupal\Core\Entity\EntityInterface $entity */
905 $entity = $data[$type];
907 foreach ($tokens as $name => $original) {
910 if (_token_module($type, 'url') === 'token' && !$entity->isNew() && $entity->hasLinkTemplate('canonical')) {
911 $replacements[$original] = $entity->toUrl('canonical')->toString();
916 if (_token_module($type, 'original') == 'token' && !empty($entity->original)) {
917 $label = $entity->original->label();
918 $replacements[$original] = $label;
924 // [entity:url:*] chained tokens.
925 if (($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url')) && _token_module($type, 'url') == 'token') {
926 $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $entity->toUrl()], $options, $bubbleable_metadata);
929 // [entity:original:*] chained tokens.
930 if (($original_tokens = \Drupal::token()->findWithPrefix($tokens, 'original')) && _token_module($type, 'original') == 'token' && !empty($entity->original)) {
931 $replacements += \Drupal::token()->generate($type, $original_tokens, [$type => $entity->original], $options, $bubbleable_metadata);
934 // Pass through to an generic 'entity' token type generation.
936 'entity_type' => $entity_type,
938 'token_type' => $type,
940 // @todo Investigate passing through more data like everything from entity_extract_ids().
941 $replacements += \Drupal::token()->generate('entity', $tokens, $entity_data, $options, $bubbleable_metadata);
945 if ($type == 'array' && !empty($data['array']) && is_array($data['array'])) {
946 $array = $data['array'];
948 $sort = isset($options['array sort']) ? $options['array sort'] : TRUE;
949 $keys = token_element_children($array, $sort);
951 /** @var \Drupal\Core\Render\RendererInterface $renderer */
952 $renderer = \Drupal::service('renderer');
954 foreach ($tokens as $name => $original) {
957 $value = $array[$keys[0]];
958 $value = is_array($value) ? $renderer->renderPlain($value) : (string) $value;
959 $replacements[$original] = $value;
963 $value = $array[$keys[count($keys) - 1]];
964 $value = is_array($value) ? $renderer->renderPlain($value) : (string) $value;
965 $replacements[$original] = $value;
969 $replacements[$original] = count($keys);
973 $replacements[$original] = token_render_array($keys, $options);
977 $reversed = array_reverse($array, TRUE);
978 $replacements[$original] = token_render_array($reversed, $options);
982 $replacements[$original] = token_render_array($array, ['join' => ''] + $options);
987 // [array:value:*] dynamic tokens.
988 if ($value_tokens = \Drupal::token()->findWithPrefix($tokens, 'value')) {
989 foreach ($value_tokens as $key => $original) {
990 if ($key[0] !== '#' && isset($array[$key])) {
991 $replacements[$original] = token_render_array_value($array[$key], $options);
996 // [array:join:*] dynamic tokens.
997 if ($join_tokens = \Drupal::token()->findWithPrefix($tokens, 'join')) {
998 foreach ($join_tokens as $join => $original) {
999 $replacements[$original] = token_render_array($array, ['join' => $join] + $options);
1003 // [array:keys:*] chained tokens.
1004 if ($key_tokens = \Drupal::token()->findWithPrefix($tokens, 'keys')) {
1005 $replacements += \Drupal::token()->generate('array', $key_tokens, ['array' => $keys], $options, $bubbleable_metadata);
1008 // [array:reversed:*] chained tokens.
1009 if ($reversed_tokens = \Drupal::token()->findWithPrefix($tokens, 'reversed')) {
1010 $replacements += \Drupal::token()->generate('array', $reversed_tokens, ['array' => array_reverse($array, TRUE)], ['array sort' => FALSE] + $options, $bubbleable_metadata);
1013 // @todo Handle if the array values are not strings and could be chained.
1017 if ($type == 'random') {
1018 foreach ($tokens as $name => $original) {
1021 $replacements[$original] = mt_rand();
1026 // [custom:hash:*] dynamic token.
1027 if ($hash_tokens = \Drupal::token()->findWithPrefix($tokens, 'hash')) {
1028 $algos = hash_algos();
1029 foreach ($hash_tokens as $name => $original) {
1030 if (in_array($name, $algos)) {
1031 $replacements[$original] = hash($name, Crypt::randomBytes(55));
1037 // If $type is a token type, $data[$type] is empty but $data[$entity_type] is
1038 // not, re-run token replacements.
1039 if (empty($data[$type]) && ($entity_type = \Drupal::service('token.entity_mapper')->getEntityTypeForTokenType($type)) && $entity_type != $type && !empty($data[$entity_type]) && empty($options['recursive'])) {
1040 $data[$type] = $data[$entity_type];
1041 $options['recursive'] = TRUE;
1042 $replacements += \Drupal::moduleHandler()->invokeAll('tokens', [$type, $tokens, $data, $options, $bubbleable_metadata]);
1045 // If the token type specifics a 'needs-data' value, and the value is not
1046 // present in $data, then throw an error.
1047 if (!empty($GLOBALS['drupal_test_info']['test_run_id'])) {
1048 // Only check when tests are running.
1049 $type_info = \Drupal::token()->getTypeInfo($type);
1050 if (!empty($type_info['needs-data']) && !isset($data[$type_info['needs-data']])) {
1051 trigger_error(t('Attempting to perform token replacement for token type %type without required data', ['%type' => $type]), E_USER_WARNING);
1055 return $replacements;
1059 * Implements hook_token_info() on behalf of book.module.
1061 function book_token_info() {
1062 $info['types']['book'] = [
1063 'name' => t('Book'),
1064 'description' => t('Tokens related to books.'),
1065 'needs-data' => 'book',
1068 $info['tokens']['book']['title'] = [
1069 'name' => t('Title'),
1070 'description' => t('Title of the book.'),
1072 $info['tokens']['book']['author'] = [
1073 'name' => t('Author'),
1074 'description' => t('The author of the book.'),
1077 $info['tokens']['book']['root'] = [
1078 'name' => t('Root'),
1079 'description' => t('Top level of the book.'),
1082 $info['tokens']['book']['parent'] = [
1083 'name' => t('Parent'),
1084 'description' => t('Parent of the current page.'),
1087 $info['tokens']['book']['parents'] = [
1088 'name' => t('Parents'),
1089 'description' => t("An array of all the node's parents, starting with the root."),
1093 $info['tokens']['node']['book'] = [
1094 'name' => t('Book'),
1095 'description' => t('The book page associated with the node.'),
1102 * Implements hook_tokens() on behalf of book.module.
1104 function book_tokens($type, $tokens, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata) {
1108 if ($type == 'node' && !empty($data['node'])) {
1109 $book = $data['node']->book;
1111 if (!empty($book['bid'])) {
1112 if ($book_tokens = \Drupal::token()->findWithPrefix($tokens, 'book')) {
1113 $child_node = Node::load($book['nid']);
1114 $replacements += \Drupal::token()->generate('book', $book_tokens, ['book' => $child_node], $options, $bubbleable_metadata);
1119 elseif ($type == 'book' && !empty($data['book'])) {
1120 $book = $data['book']->book;
1122 if (!empty($book['bid'])) {
1123 $book_node = Node::load($book['bid']);
1125 foreach ($tokens as $name => $original) {
1129 $replacements[$original] = $book_node->getTitle();
1133 if (!empty($book['pid'])) {
1134 $parent_node = Node::load($book['pid']);
1135 $replacements[$original] = $parent_node->getTitle();
1140 if ($parents = token_book_load_all_parents($book)) {
1141 $replacements[$original] = token_render_array($parents, $options);
1147 if ($book_tokens = \Drupal::token()->findWithPrefix($tokens, 'author')) {
1148 $replacements += \Drupal::token()->generate('user', $book_tokens, ['user' => $book_node->getOwner()], $options, $bubbleable_metadata);
1150 if ($book_tokens = \Drupal::token()->findWithPrefix($tokens, 'root')) {
1151 $replacements += \Drupal::token()->generate('node', $book_tokens, ['node' => $book_node], $options, $bubbleable_metadata);
1153 if (!empty($book['pid']) && $book_tokens = \Drupal::token()->findWithPrefix($tokens, 'parent')) {
1154 $parent_node = Node::load($book['pid']);
1155 $replacements += \Drupal::token()->generate('node', $book_tokens, ['node' => $parent_node], $options, $bubbleable_metadata);
1157 if ($book_tokens = \Drupal::token()->findWithPrefix($tokens, 'parents')) {
1158 $parents = token_book_load_all_parents($book);
1159 $replacements += \Drupal::token()->generate('array', $book_tokens, ['array' => $parents], $options, $bubbleable_metadata);
1164 return $replacements;
1168 * Implements hook_token_info() on behalf of menu_ui.module.
1170 function menu_ui_token_info() {
1172 $info['types']['menu'] = [
1173 'name' => t('Menus'),
1174 'description' => t('Tokens related to menus.'),
1175 'needs-data' => 'menu',
1177 $info['tokens']['menu']['name'] = [
1178 'name' => t('Name'),
1179 'description' => t("The name of the menu."),
1181 $info['tokens']['menu']['machine-name'] = [
1182 'name' => t('Machine-readable name'),
1183 'description' => t("The unique machine-readable name of the menu."),
1185 $info['tokens']['menu']['description'] = [
1186 'name' => t('Description'),
1187 'description' => t('The optional description of the menu.'),
1189 $info['tokens']['menu']['menu-link-count'] = [
1190 'name' => t('Menu link count'),
1191 'description' => t('The number of menu links belonging to the menu.'),
1193 $info['tokens']['menu']['edit-url'] = [
1194 'name' => t('Edit URL'),
1195 'description' => t("The URL of the menu's edit page."),
1198 $info['tokens']['menu-link']['menu'] = [
1199 'name' => t('Menu'),
1200 'description' => t('The menu of the menu link.'),
1203 $info['tokens']['menu-link']['edit-url'] = [
1204 'name' => t('Edit URL'),
1205 'description' => t("The URL of the menu link's edit page."),
1207 $info['tokens']['node']['menu-link'] = [
1208 'name' => t('Menu link'),
1209 'description' => t("The menu link for this node."),
1210 'type' => 'menu-link',
1217 * Implements hook_tokens() on behalf of menu_ui.module.
1219 function menu_ui_tokens($type, $tokens, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata) {
1222 /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
1223 $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
1225 $url_options = ['absolute' => TRUE];
1226 if (isset($options['langcode'])) {
1227 $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
1228 $langcode = $options['langcode'];
1235 if ($type == 'node' && !empty($data['node'])) {
1236 /** @var \Drupal\node\NodeInterface $node */
1237 $node = $data['node'];
1239 foreach ($tokens as $name => $original) {
1242 // On node-form save we populate a calculated field with a menu_link
1244 // @see token_node_menu_link_submit()
1245 if ($node->getFieldDefinition('menu_link') && $menu_link = $node->menu_link->entity) {
1246 /** @var \Drupal\menu_link_content\MenuLinkContentInterface $menu_link */
1247 $replacements[$original] = $menu_link->getTitle();
1250 $url = $node->toUrl();
1251 if ($links = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $url->getRouteParameters())) {
1252 $link = _token_menu_link_best_match($node, $links);
1253 $replacements[$original] = token_menu_link_translated_title($link, $langcode);
1259 // Chained token relationships.
1260 if ($menu_tokens = \Drupal::token()->findWithPrefix($tokens, 'menu-link')) {
1261 if ($node->getFieldDefinition('menu_link') && $menu_link = $node->menu_link->entity) {
1262 /** @var \Drupal\menu_link_content\MenuLinkContentInterface $menu_link */
1263 $replacements += \Drupal::token()->generate('menu-link', $menu_tokens, ['menu-link' => $menu_link], $options, $bubbleable_metadata);
1266 $url = $node->toUrl();
1267 if ($links = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $url->getRouteParameters())) {
1268 $link = _token_menu_link_best_match($node, $links);
1269 $replacements += \Drupal::token()->generate('menu-link', $menu_tokens, ['menu-link' => $link], $options, $bubbleable_metadata);
1276 // Menu link tokens.
1277 if ($type == 'menu-link' && !empty($data['menu-link'])) {
1278 /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
1279 $link = $data['menu-link'];
1281 if ($link instanceof MenuLinkContentInterface) {
1282 $link = $menu_link_manager->createInstance($link->getPluginId());
1285 foreach ($tokens as $name => $original) {
1288 if ($menu = Menu::load($link->getMenuName())) {
1289 $replacements[$original] = $menu->label();
1294 $replacements[$original] = $link->getEditRoute()->setOptions($url_options)->toString();
1299 // Chained token relationships.
1300 if (($menu_tokens = \Drupal::token()->findWithPrefix($tokens, 'menu')) && $menu = Menu::load($link->getMenuName())) {
1301 $replacements += \Drupal::token()->generate('menu', $menu_tokens, ['menu' => $menu], $options, $bubbleable_metadata);
1306 if ($type == 'menu' && !empty($data['menu'])) {
1307 /** @var \Drupal\system\MenuInterface $menu */
1308 $menu = $data['menu'];
1310 foreach ($tokens as $name => $original) {
1313 $replacements[$original] = $menu->label();
1316 case 'machine-name':
1317 $replacements[$original] = $menu->id();
1321 $replacements[$original] = $menu->getDescription();
1324 case 'menu-link-count':
1325 $replacements[$original] = $menu_link_manager->countMenuLinks($menu->id());
1329 $replacements[$original] = Url::fromRoute('entity.menu.edit_form', ['menu' => $menu->id()], $url_options)->toString();
1335 return $replacements;
1339 * Returns a best matched link for a given node.
1341 * If the url exists in multiple menus, default to the one set on the node
1344 * @param \Drupal\node\NodeInterface $node
1345 * The node to look up the default menu settings from.
1346 * @param array $links
1347 * An array of instances keyed by plugin ID.
1349 * @return \Drupal\Core\Menu\MenuLinkInterface
1352 function _token_menu_link_best_match(NodeInterface $node, array $links) {
1353 // Get the menu ui defaults so we can determine what menu was
1354 // selected for this node. This ensures that if the node was added
1355 // to the menu via the node UI, we use that as a default. If it
1356 // was not added via the node UI then grab the first in the
1358 $defaults = menu_ui_get_menu_link_defaults($node);
1359 if (isset($defaults['id']) && isset($links[$defaults['id']])) {
1360 $link = $links[$defaults['id']];
1363 $link = reset($links);
1369 * Implements hook_token_info_alter() on behalf of field.module.
1371 * We use hook_token_info_alter() rather than hook_token_info() as other
1372 * modules may already have defined some field tokens.
1374 function field_token_info_alter(&$info) {
1375 $type_info = \Drupal::service('plugin.manager.field.field_type')->getDefinitions();
1377 // Attach field tokens to their respecitve entity tokens.
1378 foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
1379 if (!$entity_type->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface')) {
1383 // Make sure a token type exists for this entity.
1384 $token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($entity_type_id);
1385 if (empty($token_type) || !isset($info['types'][$token_type])) {
1389 $fields = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($entity_type_id);
1390 foreach ($fields as $field_name => $field) {
1391 /** @var \Drupal\field\FieldStorageConfigInterface $field */
1393 // Ensure the token implements FieldStorageConfigInterface or is defined
1396 if (isset($info['types'][$token_type]['module'])) {
1397 $provider = $info['types'][$token_type]['module'];
1399 if (!($field instanceof FieldStorageConfigInterface) && $provider != 'token') {
1403 // If a token already exists for this field, then don't add it.
1404 if (isset($info['tokens'][$token_type][$field_name])) {
1408 if ($token_type == 'comment' && $field_name == 'comment_body') {
1409 // Core provides the comment field as [comment:body].
1413 // Do not define the token type if the field has no properties.
1414 if (!$field->getPropertyDefinitions()) {
1418 // Generate a description for the token.
1419 $labels = _token_field_label($entity_type_id, $field_name);
1420 $label = array_shift($labels);
1421 $params['@type'] = $type_info[$field->getType()]['label'];
1422 if (!empty($labels)) {
1423 $params['%labels'] = implode(', ', $labels);
1424 $description = t('@type field. Also known as %labels.', $params);
1427 $description = t('@type field.', $params);
1430 $cardinality = $field->getCardinality();
1431 $cardinality = ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || $cardinality > 3) ? 3 : $cardinality;
1432 $field_token_name = $token_type . '-' . $field_name;
1433 $info['tokens'][$token_type][$field_name] = [
1434 'name' => Html::escape($label),
1435 'description' => $description,
1436 'module' => 'token',
1437 // For multivalue fields the field token is a list type.
1438 'type' => $cardinality > 1 ? "list<$field_token_name>" : $field_token_name,
1441 // Field token type.
1442 $info['types'][$field_token_name] = [
1443 'name' => Html::escape($label),
1444 'description' => t('@label tokens.', ['@label' => Html::escape($label)]),
1445 'needs-data' => $field_token_name,
1448 // Field list token type.
1449 if ($cardinality > 1) {
1450 $info['types']["list<$field_token_name>"] = [
1451 'name' => t('List of @type values', ['@type' => Html::escape($label)]),
1452 'description' => t('Tokens for lists of @type values.', ['@type' => Html::escape($label)]),
1453 'needs-data' => "list<$field_token_name>",
1458 // Show a different token for each field delta.
1459 if ($cardinality > 1) {
1460 for ($delta = 0; $delta < $cardinality; $delta++) {
1461 $info['tokens']["list<$field_token_name>"][$delta] = [
1462 'name' => t('@type type with delta @delta', ['@type' => Html::escape($label), '@delta' => $delta]),
1463 'module' => 'token',
1464 'type' => $field_token_name,
1470 foreach ($field->getPropertyDefinitions() as $property => $property_definition) {
1471 if (is_subclass_of($property_definition->getClass(), 'Drupal\Core\TypedData\PrimitiveInterface')) {
1472 $info['tokens'][$field_token_name][$property] = [
1473 'name' => $property_definition->getLabel(),
1474 'description' => $property_definition->getDescription(),
1475 'module' => 'token',
1478 elseif (($property_definition instanceof DataReferenceDefinitionInterface) && ($property_definition->getTargetDefinition() instanceof EntityDataDefinitionInterface)) {
1479 $referenced_entity_type = $property_definition->getTargetDefinition()->getEntityTypeId();
1480 $referenced_token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($referenced_entity_type);
1481 $info['tokens'][$field_token_name][$property] = [
1482 'name' => $property_definition->getLabel(),
1483 'description' => $property_definition->getDescription(),
1484 'module' => 'token',
1485 'type' => $referenced_token_type,
1489 // Provide image_with_image_style tokens for image fields.
1490 if ($field->getType() == 'image') {
1491 $image_styles = image_style_options(FALSE);
1492 foreach ($image_styles as $style => $description) {
1493 $info['tokens'][$field_token_name][$style] = [
1494 'name' => $description,
1495 'description' => t('Represents the image in the given image style.'),
1496 'type' => 'image_with_image_style',
1500 // Provide format token for datetime fields.
1501 if ($field->getType() == 'datetime') {
1502 $info['tokens'][$field_token_name]['date'] = $info['tokens'][$field_token_name]['value'];
1503 $info['tokens'][$field_token_name]['date']['name'] .= ' ' . t('format');
1504 $info['tokens'][$field_token_name]['date']['type'] = 'date';
1506 if ($field->getType() == 'daterange' || $field->getType() == 'date_recur') {
1507 $info['tokens'][$field_token_name]['start_date'] = $info['tokens'][$field_token_name]['value'];
1508 $info['tokens'][$field_token_name]['start_date']['name'] .= ' ' . t('format');
1509 $info['tokens'][$field_token_name]['start_date']['type'] = 'date';
1510 $info['tokens'][$field_token_name]['end_date'] = $info['tokens'][$field_token_name]['end_value'];
1511 $info['tokens'][$field_token_name]['end_date']['name'] .= ' ' . t('format');
1512 $info['tokens'][$field_token_name]['end_date']['type'] = 'date';
1519 * Returns the label of a certain field.
1521 * Therefore it looks up in all bundles to find the most used instance.
1523 * Based on views_entity_field_label().
1525 * @todo Resync this method with views_entity_field_label().
1527 function _token_field_label($entity_type, $field_name) {
1529 // Count the amount of instances per label per field.
1530 foreach (array_keys(\Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type)) as $bundle) {
1531 $bundle_instances = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type, $bundle);
1532 if (isset($bundle_instances[$field_name])) {
1533 $instance = $bundle_instances[$field_name];
1534 $label = (string) $instance->getLabel();
1535 $labels[$label] = isset($labels[$label]) ? ++$labels[$label] : 1;
1539 if (empty($labels)) {
1540 return [$field_name];
1543 // Sort the field labels by it most used label and return the labels.
1545 return array_keys($labels);
1549 * Implements hook_tokens() on behalf of field.module.
1551 function field_tokens($type, $tokens, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata) {
1553 $langcode = isset($options['langcode']) ? $options['langcode'] : NULL;
1555 if ($type == 'entity' && !empty($data['entity_type']) && !empty($data['entity']) && !empty($data['token_type'])) {
1556 /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
1557 $entity = $data['entity'];
1558 if (!($entity instanceof ContentEntityInterface)) {
1559 return $replacements;
1562 if (!isset($options['langcode'])) {
1563 // Set the active language in $options, so that it is passed along.
1564 $langcode = $options['langcode'] = $entity->language()->getId();
1566 // Obtain the entity with the correct language.
1567 $entity = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode);
1569 $view_mode_name = $entity->getEntityTypeId() . '.' . $entity->bundle() . '.token';
1570 $view_display = \Drupal::entityTypeManager()->getStorage('entity_view_display')->load($view_mode_name);
1571 $token_view_display = (!empty($view_display) && $view_display->status());
1572 foreach ($tokens as $name => $original) {
1573 // For the [entity:field_name] token.
1574 if (strpos($name, ':') === FALSE) {
1575 $field_name = $name;
1576 $token_name = $name;
1578 // For [entity:field_name:0], [entity:field_name:0:value] and
1579 // [entity:field_name:value] tokens.
1581 list($field_name, $delta) = explode(':', $name, 2);
1582 if (!is_numeric($delta)) {
1585 $token_name = $field_name;
1587 // Ensure the entity has the requested field and that the token for it is
1588 // defined by token.module.
1589 if (!$entity->hasField($field_name) || _token_module($data['token_type'], $token_name) != 'token') {
1593 $display_options = 'token';
1594 // Do not continue if the field is empty.
1595 if ($entity->get($field_name)->isEmpty()) {
1598 // Handle [entity:field_name] and [entity:field_name:0] tokens.
1599 if ($field_name === $name || isset($delta)) {
1600 if (!$token_view_display) {
1601 // We don't have the token view display and should fall back on
1602 // default formatters. If the field has specified a specific formatter
1603 // to be used by default with tokens, use that, otherwise use the
1604 // default formatter.
1605 /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
1606 $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
1607 $field_type_definition = $field_type_manager->getDefinition($entity->getFieldDefinition($field_name)->getType());
1608 $display_options = [
1609 'type' => !empty($field_type_definition['default_token_formatter']) ? $field_type_definition['default_token_formatter'] : $field_type_definition['default_formatter'],
1610 'label' => 'hidden',
1614 // Render only one delta.
1615 if (isset($delta)) {
1616 if ($field_delta = $entity->{$field_name}[$delta]) {
1617 $field_output = $field_delta->view($display_options);
1619 // If no such delta exists, let's not replace the token.
1624 // Render the whole field (with all deltas).
1626 $field_output = $entity->$field_name->view($display_options);
1627 // If we are displaying all field items we need this #pre_render
1629 $field_output['#pre_render'][] = 'token_pre_render_field_token';
1631 $field_output['#token_options'] = $options;
1632 $replacements[$original] = \Drupal::service('renderer')->renderPlain($field_output);
1634 // Handle [entity:field_name:value] and [entity:field_name:0:value]
1636 elseif ($field_tokens = \Drupal::token()->findWithPrefix($tokens, $field_name)) {
1637 $property_token_data = [
1638 'field_property' => TRUE,
1639 $data['entity_type'] . '-' . $field_name => $entity->$field_name,
1640 'field_name' => $data['entity_type'] . '-' . $field_name,
1642 $replacements += \Drupal::token()->generate($field_name, $field_tokens, $property_token_data, $options, $bubbleable_metadata);
1646 // Remove the cloned object from memory.
1649 elseif (!empty($data['field_property'])) {
1650 foreach ($tokens as $token => $original) {
1651 $filtered_tokens = $tokens;
1653 $parts = explode(':', $token);
1654 if (is_numeric($parts[0])) {
1655 if (count($parts) > 1) {
1657 $property_name = $parts[1];
1658 // Pre-filter the tokens to select those with the correct delta.
1659 $filtered_tokens = \Drupal::token()->findWithPrefix($tokens, $delta);
1660 // Remove the delta to unify between having and not having one.
1661 array_shift($parts);
1664 // Token is fieldname:delta, which is invalid.
1669 $property_name = $parts[0];
1672 if (isset($data[$data['field_name']][$delta])) {
1673 $field_item = $data[$data['field_name']][$delta];
1676 // The field has no such delta, abort replacement.
1680 if (isset($field_item->$property_name) && ($field_item->$property_name instanceof FieldableEntityInterface)) {
1681 // Entity reference field.
1682 $entity = $field_item->$property_name;
1683 // Obtain the referenced entity with the correct language.
1684 $entity = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode);
1686 if (count($parts) > 1) {
1687 $field_tokens = \Drupal::token()->findWithPrefix($filtered_tokens, $property_name);
1688 $token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($entity->getEntityTypeId(), TRUE);
1689 $replacements += \Drupal::token()->generate($token_type, $field_tokens, [$token_type => $entity], $options, $bubbleable_metadata);
1692 $replacements[$original] = $entity->label();
1695 elseif (($field_item->getFieldDefinition()->getType() == 'image') && ($style = ImageStyle::load($property_name))) {
1696 // Handle [node:field_name:image_style:property] tokens and multivalued
1697 // [node:field_name:delta:image_style:property] tokens. If the token is
1698 // of the form [node:field_name:image_style], provide the URL as a
1700 $property_name = isset($parts[1]) ? $parts[1] : 'url';
1701 $entity = $field_item->entity;
1702 if (!empty($field_item->entity)) {
1703 $original_uri = $entity->getFileUri();
1705 // Only generate the image derivative if needed.
1706 if ($property_name === 'width' || $property_name === 'height') {
1708 'width' => $field_item->width,
1709 'height' => $field_item->height,
1711 $style->transformDimensions($dimensions, $original_uri);
1712 $replacements[$original] = $dimensions[$property_name];
1714 elseif ($property_name === 'uri') {
1715 $replacements[$original] = $style->buildUri($original_uri);
1717 elseif ($property_name === 'url') {
1718 $replacements[$original] = $style->buildUrl($original_uri);
1721 // Generate the image derivative, if it doesn't already exist.
1722 $derivative_uri = $style->buildUri($original_uri);
1723 $derivative_exists = TRUE;
1724 if (!file_exists($derivative_uri)) {
1725 $derivative_exists = $style->createDerivative($original_uri, $derivative_uri);
1727 if ($derivative_exists) {
1728 $image = \Drupal::service('image.factory')->get($derivative_uri);
1729 // Provide the replacement.
1730 switch ($property_name) {
1732 $replacements[$original] = $image->getMimeType();
1736 $replacements[$original] = $image->getFileSize();
1743 elseif (in_array($field_item->getFieldDefinition()->getType(), ['datetime', 'daterange', 'date_recur']) && in_array($property_name, ['date', 'start_date', 'end_date']) && !empty($field_item->$property_name)) {
1744 $timestamp = $field_item->{$property_name}->getTimestamp();
1745 // If the token is an exact match for the property or the delta and the
1746 // property, use the timestamp as-is.
1747 if ($property_name == $token || "$delta:$property_name" == $token) {
1748 $replacements[$original] = $timestamp;
1751 $date_tokens = \Drupal::token()->findWithPrefix($filtered_tokens, $property_name);
1752 $replacements += \Drupal::token()->generate('date', $date_tokens, ['date' => $timestamp], $options, $bubbleable_metadata);
1756 $replacements[$original] = $field_item->$property_name;
1760 return $replacements;
1764 * Pre-render callback for field output used with tokens.
1766 function token_pre_render_field_token($elements) {
1767 // Remove the field theme hook, attachments, and JavaScript states.
1768 unset($elements['#theme']);
1769 unset($elements['#states']);
1770 unset($elements['#attached']);
1772 // Prevent multi-value fields from appearing smooshed together by appending
1773 // a join suffix to all but the last value.
1774 $deltas = Element::getVisibleChildren($elements);
1775 $count = count($deltas);
1777 $join = isset($elements['#token_options']['join']) ? $elements['#token_options']['join'] : ", ";
1778 foreach ($deltas as $index => $delta) {
1779 // Do not add a suffix to the last item.
1780 if ($index < ($count - 1)) {
1781 $elements[$delta] += ['#suffix' => $join];