5 * Enhances the token API in core: adds a browseable UI, missing tokens, etc.
8 use Drupal\Component\Render\PlainTextOutput;
9 use Drupal\Core\Block\BlockPluginInterface;
10 use Drupal\Core\Form\FormStateInterface;
11 use Drupal\Core\Menu\MenuLinkInterface;
12 use Drupal\Core\Render\BubbleableMetadata;
13 use Drupal\Core\Render\Element;
14 use Drupal\Core\Routing\RouteMatchInterface;
15 use Drupal\Core\Entity\EntityTypeInterface;
16 use Drupal\Core\Field\BaseFieldDefinition;
17 use Drupal\Core\TypedData\TranslatableInterface;
18 use Drupal\menu_link_content\Entity\MenuLinkContent;
19 use Drupal\menu_link_content\MenuLinkContentInterface;
20 use Drupal\node\Entity\Node;
21 use Drupal\node\NodeInterface;
24 * Implements hook_help().
26 function token_help($route_name, RouteMatchInterface $route_match) {
27 if ($route_name == 'help.page.token') {
28 $token_tree = \Drupal::service('token.tree_builder')->buildAllRenderable([
29 'click_insert' => FALSE,
30 'show_restricted' => TRUE,
31 'show_nested' => FALSE,
33 $output = '<h3>' . t('About') . '</h3>';
34 $output .= '<p>' . t('The <a href=":project">Token</a> module provides a user interface for the site token system. It also adds some additional tokens that are used extensively during site development. Tokens are specially formatted chunks of text that serve as placeholders for a dynamically generated value. For more information, covering both the token system and the additional tools provided by the Token module, see the <a href=":online">online documentation</a>.', [':online' => 'https://www.drupal.org/documentation/modules/token', ':project' => 'https://www.drupal.org/project/token']) . '</p>';
35 $output .= '<h3>' . t('Uses') . '</h3>';
36 $output .= '<p>' . t('Your website uses a shared token system for exposing and using placeholder tokens and their appropriate replacement values. This allows for any module to provide placeholder tokens for strings without having to reinvent the wheel. It also ensures consistency in the syntax used for tokens, making the system as a whole easier for end users to use.') . '</p>';
38 $output .= '<dt>' . t('The list of the currently available tokens on this site are shown below.') . '</dt>';
39 $output .= '<dd>' . \Drupal::service('renderer')->render($token_tree) . '</dd>';
46 * Return an array of the core modules supported by token.module.
48 function _token_core_supported_modules() {
49 return ['book', 'field', 'menu_ui'];
53 * Implements hook_theme().
55 function token_theme() {
56 $info['token_tree_link'] = [
59 'global_types' => TRUE,
60 'click_insert' => TRUE,
61 'show_restricted' => FALSE,
62 'show_nested' => FALSE,
63 'recursion_limit' => 3,
67 'file' => 'token.pages.inc',
74 * Implements hook_block_view_alter().
76 function token_block_view_alter(&$build, BlockPluginInterface $block) {
77 if (isset($build['#configuration'])) {
78 $label = $build['#configuration']['label'];
79 if ($label != '<none>') {
80 // The label is automatically escaped, avoid escaping it twice.
81 // @todo https://www.drupal.org/node/2580723 will add a method or option
82 // to the token API to do this, use that when available.
83 $bubbleable_metadata = BubbleableMetadata::createFromRenderArray($build);
84 $build['#configuration']['label'] = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($label, [], [], $bubbleable_metadata));
85 $bubbleable_metadata->applyTo($build);
91 * Implements hook_form_FORM_ID_alter().
93 function token_form_block_form_alter(&$form, FormStateInterface $form_state) {
95 '#theme' => 'token_tree_link',
98 $rendered_token_tree = \Drupal::service('renderer')->render($token_tree);
99 $form['settings']['label']['#description'] = t('This field supports tokens. @browse_tokens_link', [
100 '@browse_tokens_link' => $rendered_token_tree,
102 $form['settings']['label']['#element_validate'][] = 'token_element_validate';
103 $form['settings']['label'] += ['#token_types' => []];
107 * Implements hook_field_info_alter().
109 function token_field_info_alter(&$info) {
111 'taxonomy_term_reference' => 'taxonomy_term_reference_plain',
112 'number_integer' => 'number_unformatted',
113 'number_decimal' => 'number_unformatted',
114 'number_float' => 'number_unformatted',
115 'file' => 'file_url_plain',
116 'image' => 'file_url_plain',
117 'text' => 'text_default',
118 'text_long' => 'text_default',
119 'text_with_summary' => 'text_default',
120 'list_integer' => 'list_default',
121 'list_float' => 'list_default',
122 'list_string' => 'list_default',
123 'list_boolean' => 'list_default',
125 foreach ($defaults as $field_type => $default_token_formatter) {
126 if (isset($info[$field_type])) {
127 $info[$field_type] += ['default_token_formatter' => $default_token_formatter];
133 * Implements hook_date_format_insert().
135 function token_date_format_insert() {
140 * Implements hook_date_format_delete().
142 function token_date_format_delete() {
147 * Implements hook_field_storage_config_presave().
149 function token_field_config_presave($instance) {
154 * Implements hook_field_storage_config_delete().
156 function token_field_config_delete($instance) {
161 * Clear token caches and static variables.
163 function token_clear_cache() {
164 \Drupal::token()->resetInfo();
165 \Drupal::service('token.entity_mapper')->resetInfo();
166 drupal_static_reset('token_menu_link_load_all_parents');
167 drupal_static_reset('token_book_link_load');
171 * Implements hook_entity_type_alter().
173 * Because some token types to do not match their entity type names, we have to
174 * map them to the proper type. This is purely for other modules' benefit.
176 * @see \Drupal\token\TokenEntityMapperInterface::getEntityTypeMappings()
177 * @see http://drupal.org/node/737726
179 function token_entity_type_alter(array &$entity_types) {
180 $devel_exists = \Drupal::moduleHandler()->moduleExists('devel');
181 /* @var $entity_types EntityTypeInterface[] */
182 foreach ($entity_types as $entity_type_id => $entity_type) {
183 if (!$entity_type->get('token_type')) {
184 // Fill in default token types for entities.
185 switch ($entity_type_id) {
186 case 'taxonomy_term':
187 case 'taxonomy_vocabulary':
188 // Stupid taxonomy token types...
189 $entity_type->set('token_type', str_replace('taxonomy_', '', $entity_type_id));
193 // By default the token type is the same as the entity type.
194 $entity_type->set('token_type', $entity_type_id);
200 && $entity_type->hasViewBuilderClass()
201 && ($canonical = $entity_type->getLinkTemplate('canonical'))
202 && !$entity_type->hasLinkTemplate('token-devel')) {
203 $entity_type->setLinkTemplate('token-devel', $canonical . '/devel/token');
209 * Implements hook_entity_view_modes_info().
213 * Implements hook_module_implements_alter().
215 * Adds missing token support for core modules.
217 function token_module_implements_alter(&$implementations, $hook) {
218 \Drupal::moduleHandler()->loadInclude('token', 'inc', 'token.tokens');
220 if ($hook == 'tokens' || $hook == 'token_info' || $hook == 'token_info_alter' || $hook == 'tokens_alter') {
221 foreach (_token_core_supported_modules() as $module) {
222 if (\Drupal::moduleHandler()->moduleExists($module) && function_exists($module . '_' . $hook)) {
223 $implementations[$module] = TRUE;
226 // Move token.module to get included first since it is responsible for
228 if (isset($implementations['token'])) {
229 unset($implementations['token']);
230 $implementations = array_merge(['token' => 'tokens'], $implementations);
236 * Return the module responsible for a token.
238 * @param string $type
240 * @param string $name
244 * The value of $info['tokens'][$type][$name]['module'] from token info, or
245 * NULL if the value does not exist.
247 function _token_module($type, $name) {
248 $token_info = \Drupal::token()->getTokenInfo($type, $name);
249 return isset($token_info['module']) ? $token_info['module'] : NULL;
253 * Validate a form element that should have tokens in it.
255 * Form elements that want to add this validation should have the #token_types
260 * $form['my_node_text_element'] = [
261 * '#type' => 'textfield',
262 * '#title' => t('Some text to token-ize that has a node context.'),
263 * '#default_value' => 'The title of this node is [node:title].',
264 * '#element_validate' => ['token_element_validate'],
265 * '#token_types' => ['node'],
266 * '#min_tokens' => 1,
267 * '#max_tokens' => 10,
271 function token_element_validate($element, FormStateInterface $form_state) {
272 $value = isset($element['#value']) ? $element['#value'] : $element['#default_value'];
274 if (!mb_strlen($value)) {
275 // Empty value needs no further validation since the element should depend
276 // on using the '#required' FAPI property.
280 $tokens = \Drupal::token()->scan($value);
281 $title = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];
283 // Validate if an element must have a minimum number of tokens.
284 if (isset($element['#min_tokens']) && count($tokens) < $element['#min_tokens']) {
285 $error = \Drupal::translation()->formatPlural($element['#min_tokens'], '%name must contain at least one token.', '%name must contain at least @count tokens.', ['%name' => $title]);
286 $form_state->setError($element, $error);
289 // Validate if an element must have a maximum number of tokens.
290 if (isset($element['#max_tokens']) && count($tokens) > $element['#max_tokens']) {
291 $error = \Drupal::translation()->formatPlural($element['#max_tokens'], '%name must contain at most one token.', '%name must contain at most @count tokens.', ['%name' => $title]);
292 $form_state->setError($element, $error);
295 // Check if the field defines specific token types.
296 if (isset($element['#token_types'])) {
297 $invalid_tokens = \Drupal::token()->getInvalidTokensByContext($tokens, $element['#token_types']);
298 if ($invalid_tokens) {
299 $form_state->setError($element, t('%name is using the following invalid tokens: @invalid-tokens.', ['%name' => $title, '@invalid-tokens' => implode(', ', $invalid_tokens)]));
307 * Implements hook_form_FORM_ID_alter().
309 function token_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state) {
310 $field_config = $form_state->getFormObject()->getEntity();
311 $field_storage = $field_config->getFieldStorageDefinition();
312 if ($field_storage->isLocked()) {
315 $field_type = $field_storage->getType();
316 if (($field_type == 'file' || $field_type == 'image') && isset($form['settings']['file_directory'])) {
317 // GAH! We can only support global tokens in the upload file directory path.
318 $form['settings']['file_directory']['#element_validate'][] = 'token_element_validate';
319 // Date support needs to be implicitly added, as while technically it's not
320 // a global token, it is a not only used but is the default value.
321 // https://www.drupal.org/node/2642160
322 $form['settings']['file_directory'] += ['#token_types' => ['date']];
323 $form['settings']['file_directory']['#description'] .= ' ' . t('This field supports tokens.');
326 // Note that the description is tokenized via token_field_widget_form_alter().
327 $form['description']['#element_validate'][] = 'token_element_validate';
328 $form['description'] += ['#token_types' => []];
330 $form['token_tree'] = [
331 '#theme' => 'token_tree_link',
332 '#token_types' => [],
333 '#weight' => $form['description']['#weight'] + 0.5,
338 * Implements hook_form_BASE_FORM_ID_alter().
340 * Alters the configure action form to add token context validation and
341 * adds the token tree for a better token UI and selection.
343 function token_form_action_form_alter(&$form, $form_state) {
344 if (isset($form['plugin'])) {
345 switch ($form['plugin']['#value']) {
346 case 'action_message_action':
347 case 'action_send_email_action':
348 case 'action_goto_action':
349 $form['token_tree'] = [
350 '#theme' => 'token_tree_link',
351 '#token_types' => 'all',
354 $form['actions']['#weight'] = 101;
355 // @todo Add token validation to the action fields that can use tokens.
362 * Implements hook_form_FORM_ID_alter().
364 * Alters the user e-mail fields to add token context validation and
365 * adds the token tree for a better token UI and selection.
367 function token_form_user_admin_settings_alter(&$form, FormStateInterface $form_state) {
368 $email_token_help = t('Available variables are: [site:name], [site:url], [user:display-name], [user:account-name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:cancel-url].');
370 foreach (Element::children($form) as $key) {
371 $element = &$form[$key];
373 // Remove the crummy default token help text.
374 if (!empty($element['#description'])) {
375 $element['#description'] = trim(str_replace($email_token_help, t('The list of available tokens that can be used in e-mails is provided below.'), $element['#description']));
379 case 'email_admin_created':
380 case 'email_pending_approval':
381 case 'email_no_approval_required':
382 case 'email_password_reset':
383 case 'email_cancel_confirm':
384 // Do nothing, but allow execution to continue.
387 case 'email_activated':
388 case 'email_blocked':
389 case 'email_canceled':
390 // These fieldsets have their e-mail elements inside a 'settings'
391 // sub-element, so switch to that element instead.
392 $element = &$form[$key]['settings'];
399 foreach (Element::children($element) as $sub_key) {
400 if (!isset($element[$sub_key]['#type'])) {
403 elseif ($element[$sub_key]['#type'] == 'textfield' && substr($sub_key, -8) === '_subject') {
404 // Add validation to subject textfields.
405 $element[$sub_key]['#element_validate'][] = 'token_element_validate';
406 $element[$sub_key] += ['#token_types' => ['user']];
408 elseif ($element[$sub_key]['#type'] == 'textarea' && substr($sub_key, -5) === '_body') {
409 // Add validation to body textareas.
410 $element[$sub_key]['#element_validate'][] = 'token_element_validate';
411 $element[$sub_key] += ['#token_types' => ['user']];
416 // Add the token tree UI.
417 $form['email']['token_tree'] = [
418 '#theme' => 'token_tree_link',
419 '#token_types' => ['user'],
420 '#show_restricted' => TRUE,
421 '#show_nested' => FALSE,
427 * Prepare a string for use as a valid token name.
430 * The token name to clean.
432 * The cleaned token name.
434 function token_clean_token_name($name) {
437 if (!isset($names[$name])) {
438 $cleaned_name = strtr($name, [' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => '']);
439 $cleaned_name = preg_replace('/[^\w\-]/i', '', $cleaned_name);
440 $cleaned_name = trim($cleaned_name, '-');
441 $names[$name] = $cleaned_name;
444 return $names[$name];
448 * Do not use this function yet. Its API has not been finalized.
450 function token_render_array(array $array, array $options = []) {
453 /** @var \Drupal\Core\Render\RendererInterface $renderer */
454 $renderer = \Drupal::service('renderer');
456 foreach (token_element_children($array) as $key) {
457 $value = $array[$key];
458 $rendered[] = is_array($value) ? $renderer->renderPlain($value) : (string) $value;
460 $join = isset($options['join']) ? $options['join'] : ', ';
461 return implode($join, $rendered);
465 * Do not use this function yet. Its API has not been finalized.
467 function token_render_array_value($value, array $options = []) {
468 /** @var \Drupal\Core\Render\RendererInterface $renderer */
469 $renderer = \Drupal::service('renderer');
471 $rendered = is_array($value) ? $renderer->renderPlain($value) : (string) $value;
476 * Copy of drupal_render_cache_get() that does not care about request method.
478 function token_render_cache_get($elements) {
479 if (!$cid = drupal_render_cid_create($elements)) {
482 $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
484 if (!empty($cid) && $cache = \Drupal::cache($bin)->get($cid)) {
485 // Add additional libraries, JavaScript, CSS and other data attached
487 if (isset($cache->data['#attached'])) {
488 drupal_process_attached($cache->data);
490 // Return the rendered output.
491 return $cache->data['#markup'];
497 * Coyp of drupal_render_cache_set() that does not care about request method.
499 function token_render_cache_set(&$markup, $elements) {
500 // This should only run of drupal_render_cache_set() did not.
501 if (in_array(\Drupal::request()->server->get('REQUEST_METHOD'), ['GET', 'HEAD'])) {
505 $original_method = \Drupal::request()->server->get('REQUEST_METHOD');
506 \Drupal::request()->server->set('REQUEST_METHOD', 'GET');
507 drupal_render_cache_set($markup, $elements);
508 \Drupal::request()->server->set('REQUEST_METHOD', $original_method);
512 * Loads menu link titles for all purents of a menu link plugin ID.
514 * @param string $plugin_id
515 * The menu link plugin ID.
516 * @param string $langcode
520 * List of menu link parent titles.
522 function token_menu_link_load_all_parents($plugin_id, $langcode) {
523 $cache = &drupal_static(__FUNCTION__, []);
525 if (!isset($cache[$plugin_id][$langcode])) {
526 $cache[$plugin_id][$langcode] = [];
527 /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
528 $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
529 $parent_ids = $menu_link_manager->getParentIds($plugin_id);
530 // Remove the current plugin ID from the parents.
531 unset($parent_ids[$plugin_id]);
532 foreach ($parent_ids as $parent_id) {
533 $parent = $menu_link_manager->createInstance($parent_id);
534 $cache[$plugin_id][$langcode] = [$parent_id => token_menu_link_translated_title($parent, $langcode)] + $cache[$plugin_id][$langcode];
538 return $cache[$plugin_id][$langcode];
542 * Returns the translated link of a menu title.
544 * If the underlying entity is a content menu item, load it to get the
545 * translated menu item title.
547 * @todo Remove this when there is a better way to get a translated menu
548 * item title in core: https://www.drupal.org/node/2795143
550 * @param \Drupal\Core\Menu\MenuLinkInterface $menu_link
552 * @param string|null $langcode
553 * (optional) The langcode, defaults to the current language.
556 * The menu link title.
558 function token_menu_link_translated_title(MenuLinkInterface $menu_link, $langcode = NULL) {
559 $metadata = $menu_link->getMetaData();
560 if (isset($metadata['entity_id']) && $menu_link->getProvider() == 'menu_link_content') {
561 /** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
562 $entity = \Drupal::entityTypeManager()->getStorage('menu_link_content')->load($metadata['entity_id']);
563 $entity = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode);
564 return $entity->getTitle();
566 return $menu_link->getTitle();
570 * Loads all the parents of the term in the specified language.
574 * @param string $langcode
578 * The term parents collection.
580 function token_taxonomy_term_load_all_parents($tid, $langcode) {
581 $cache = &drupal_static(__FUNCTION__, []);
583 if (!is_numeric($tid)) {
587 if (!isset($cache[$langcode][$tid])) {
588 $cache[$langcode][$tid] = [];
589 /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
590 $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
591 $parents = $term_storage->loadAllParents($tid);
592 // Remove this term from the array.
593 array_shift($parents);
594 $parents = array_reverse($parents);
595 foreach ($parents as $term) {
596 $translation = \Drupal::service('entity.repository')->getTranslationFromContext($term, $langcode);
597 $cache[$langcode][$tid][$term->id()] = $translation->label();
601 return $cache[$langcode][$tid];
604 function token_element_children(&$elements, $sort = FALSE) {
605 // Do not attempt to sort elements which have already been sorted.
606 $sort = isset($elements['#sorted']) ? !$elements['#sorted'] : $sort;
608 // Filter out properties from the element, leaving only children.
611 foreach ($elements as $key => $value) {
612 if ($key === '' || $key[0] !== '#') {
613 $children[$key] = $value;
614 if (is_array($value) && isset($value['#weight'])) {
619 // Sort the children if necessary.
620 if ($sort && $sortable) {
621 uasort($children, 'Drupal\Component\Utility\SortArray::sortByWeightProperty');
622 // Put the sorted children back into $elements in the correct order, to
623 // preserve sorting if the same element is passed through
624 // element_children() twice.
625 foreach ($children as $key => $child) {
626 unset($elements[$key]);
627 $elements[$key] = $child;
629 $elements['#sorted'] = TRUE;
632 return array_keys($children);
636 * Loads all the parents of the book page.
639 * The book data. The 'nid' key points to the current page of the book.
640 * The 'p1' ... 'p9' keys point to parents of the page, if they exist, with 'p1'
641 * pointing to the book itself and the last defined pX to the current page.
644 * List of node titles of the book parents.
646 function token_book_load_all_parents(array $book) {
647 $cache = &drupal_static(__FUNCTION__, []);
649 if (empty($book['nid'])) {
654 if (!isset($cache[$nid])) {
657 while ($book["p$i"] != $nid) {
658 $cache[$nid][] = Node::load($book["p$i"])->getTitle();
667 * Implements hook_entity_base_field_info().
669 function token_entity_base_field_info(EntityTypeInterface $entity_type) {
670 // We add a pseudo entity-reference field to track the menu entry created
671 // from the node add/edit form so that tokens generated at that time that
672 // reference the menu link can access the yet to be saved menu link.
673 // @todo Revisit when https://www.drupal.org/node/2315773 is resolved.
674 if ($entity_type->id() === 'node' && \Drupal::moduleHandler()->moduleExists('menu_ui')) {
675 $fields['menu_link'] = BaseFieldDefinition::create('entity_reference')
676 ->setLabel(t('Menu link'))
677 ->setDescription(t('Computed menu link for the node (only available during node saving).'))
678 ->setRevisionable(TRUE)
679 ->setSetting('target_type', 'menu_link_content')
680 ->setTranslatable(TRUE)
681 ->setDisplayOptions('view', [
683 'region' => 'hidden',
686 ->setDisplayOptions('form', [
687 'region' => 'hidden',
696 * Implements hook_form_BASE_FORM_ID_alter() for node_form.
698 * Populates menu_link field on nodes from the menu item on unsaved nodes.
700 * @see menu_ui_form_node_form_submit()
701 * @see token_entity_base_field_info()
703 function token_form_node_form_alter(&$form, FormStateInterface $form_state) {
704 if (!\Drupal::moduleHandler()->moduleExists('menu_ui')) {
707 /** @var \Drupal\node\NodeForm $form_object */
708 if (!\Drupal::currentUser()->hasPermission('administer menu')) {
709 // We're only interested in when the node is unsaved and the editor has
710 // permission to create new menu links.
713 $form['#entity_builders'][] = 'token_node_menu_link_submit';
719 function token_node_menu_link_submit($entity_type, NodeInterface $node, &$form, FormStateInterface $form_state) {
720 // Entity builders run twice, once during validation and again during
721 // submission, so we only run this code after validation has been performed.
722 if (!$form_state->isValueEmpty('menu') && $form_state->getTemporaryValue('entity_validated')) {
723 $values = $form_state->getValue('menu');
724 if (!empty($values['enabled']) && trim($values['title'])) {
725 if (!empty($values['menu_parent'])) {
726 list($menu_name, $parent) = explode(':', $values['menu_parent'], 2);
727 $values['menu_name'] = $menu_name;
728 $values['parent'] = $parent;
730 // Construct an unsaved entity.
731 if ($entity_id = $form_state->getValue(['menu', 'entity_id'])) {
732 // Use the existing menu_link_content entity.
733 $entity = MenuLinkContent::load($entity_id);
734 // If the loaded MenuLinkContent doesn't have a translation for the
735 // Node's active langcode, create a new translation.
736 if ($entity->isTranslatable()) {
737 if (!$entity->hasTranslation($node->language()->getId())) {
738 $entity = $entity->addTranslation($node->language()->getId(), $entity->toArray());
741 $entity = $entity->getTranslation($node->language()->getId());
746 if ($node->isNew()) {
747 // Create a new menu_link_content entity.
748 $entity = MenuLinkContent::create([
749 // Lets just reference the UUID for now, the link is not important for
751 'link' => ['uri' => 'internal:/node/' . $node->uuid()],
752 'langcode' => $node->language()->getId(),
756 // Create a new menu_link_content entity.
757 $entity = MenuLinkContent::create([
758 'link' => ['uri' => 'entity:node/' . $node->id()],
759 'langcode' => $node->language()->getId(),
763 $entity->title->value = trim($values['title']);
764 $entity->description->value = trim($values['description']);
765 $entity->menu_name->value = $values['menu_name'];
766 $entity->parent->value = $values['parent'];
767 $entity->weight->value = isset($values['weight']) ? $values['weight'] : 0;
769 $node->menu_link = $entity;
770 // Leave this for _menu_ui_node_save() to pick up so we don't end up with
771 // duplicate menu-links.
772 $form_state->setValue(['menu', 'entity_id'], $entity->id());
778 * Implements hook_ENTITY_TYPE_insert for node entities.
780 function token_node_insert(NodeInterface $node) {
781 if ($node->hasField('menu_link') && $menu_link = $node->menu_link->entity) {
782 // Update the menu-link to point to the now saved node.
783 $menu_link->link = 'entity:node/' . $node->id();
789 * Implements hook_ENTITY_TYPE_presave() for menu_link_content.
791 function token_menu_link_content_presave(MenuLinkContentInterface $menu_link_content) {
792 drupal_static_reset('token_menu_link_load_all_parents');