5 * Enhances the token API in core: adds a browseable UI, missing tokens, etc.
8 use Drupal\Component\Render\PlainTextOutput;
9 use Drupal\Component\Utility\Unicode;
10 use Drupal\Core\Block\BlockPluginInterface;
11 use Drupal\Core\Form\FormStateInterface;
12 use Drupal\Core\Menu\MenuLinkInterface;
13 use Drupal\Core\Render\BubbleableMetadata;
14 use Drupal\Core\Render\Element;
15 use Drupal\Core\Routing\RouteMatchInterface;
16 use Drupal\Core\Entity\EntityTypeInterface;
17 use Drupal\Core\Field\BaseFieldDefinition;
18 use Drupal\Core\TypedData\TranslatableInterface;
19 use Drupal\menu_link_content\Entity\MenuLinkContent;
20 use Drupal\menu_link_content\MenuLinkContentInterface;
21 use Drupal\node\Entity\Node;
22 use Drupal\node\NodeInterface;
25 * Implements hook_help().
27 function token_help($route_name, RouteMatchInterface $route_match) {
28 if ($route_name == 'help.page.token') {
29 $token_tree = \Drupal::service('token.tree_builder')->buildAllRenderable([
30 'click_insert' => FALSE,
31 'show_restricted' => TRUE,
32 'show_nested' => FALSE,
34 $output = '<h3>' . t('About') . '</h3>';
35 $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>';
36 $output .= '<h3>' . t('Uses') . '</h3>';
37 $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>';
39 $output .= '<dt>' . t('The list of the currently available tokens on this site are shown below.') . '</dt>';
40 $output .= '<dd>' . \Drupal::service('renderer')->render($token_tree) . '</dd>';
47 * Return an array of the core modules supported by token.module.
49 function _token_core_supported_modules() {
50 return array('book', 'field', 'menu_ui');
54 * Implements hook_theme().
56 function token_theme() {
57 $info['token_tree_link'] = [
60 'global_types' => TRUE,
61 'click_insert' => TRUE,
62 'show_restricted' => FALSE,
63 'show_nested' => FALSE,
64 'recursion_limit' => 3,
68 'file' => 'token.pages.inc',
75 * Implements hook_block_view_alter().
77 function token_block_view_alter(&$build, BlockPluginInterface $block) {
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);
90 * Implements hook_form_FORM_ID_alter().
92 function token_form_block_form_alter(&$form, FormStateInterface $form_state) {
94 '#theme' => 'token_tree_link',
97 $rendered_token_tree = \Drupal::service('renderer')->render($token_tree);
98 $form['settings']['label']['#description'] =
99 t('This field supports tokens. @browse_tokens_link', ['@browse_tokens_link' => $rendered_token_tree])
101 $form['settings']['label']['#element_validate'][] = 'token_element_validate';
102 $form['settings']['label'] += ['#token_types' => []];
106 * Implements hook_field_info_alter().
108 function token_field_info_alter(&$info) {
110 'taxonomy_term_reference' => 'taxonomy_term_reference_plain',
111 'number_integer' => 'number_unformatted',
112 'number_decimal' => 'number_unformatted',
113 'number_float' => 'number_unformatted',
114 'file' => 'file_url_plain',
115 'image' => 'file_url_plain',
116 'text' => 'text_default',
117 'text_long' => 'text_default',
118 'text_with_summary' => 'text_default',
119 'list_integer' => 'list_default',
120 'list_float' => 'list_default',
121 'list_string' => 'list_default',
122 'list_boolean' => 'list_default',
124 foreach ($defaults as $field_type => $default_token_formatter) {
125 if (isset($info[$field_type])) {
126 $info[$field_type] += array('default_token_formatter' => $default_token_formatter);
132 * Implements hook_date_format_insert().
134 function token_date_format_insert() {
139 * Implements hook_date_format_delete().
141 function token_date_format_delete() {
146 * Implements hook_field_storage_config_presave().
148 function token_field_config_presave($instance) {
153 * Implements hook_field_storage_config_delete().
155 function token_field_config_delete($instance) {
160 * Clear token caches and static variables.
162 function token_clear_cache() {
163 \Drupal::token()->resetInfo();
164 \Drupal::service('token.entity_mapper')->resetInfo();
165 drupal_static_reset('token_menu_link_load_all_parents');
166 drupal_static_reset('token_book_link_load');
170 * Implements hook_entity_type_alter().
172 * Because some token types to do not match their entity type names, we have to
173 * map them to the proper type. This is purely for other modules' benefit.
175 * @see \Drupal\token\TokenEntityMapperInterface::getEntityTypeMappings()
176 * @see http://drupal.org/node/737726
178 function token_entity_type_alter(array &$entity_types) {
179 $devel_exists = \Drupal::moduleHandler()->moduleExists('devel');
180 /* @var $entity_types EntityTypeInterface[] */
181 foreach ($entity_types as $entity_type_id => $entity_type) {
182 if (!$entity_type->get('token_type')) {
183 // Fill in default token types for entities.
184 switch ($entity_type_id) {
185 case 'taxonomy_term':
186 case 'taxonomy_vocabulary':
187 // Stupid taxonomy token types...
188 $entity_type->set('token_type', str_replace('taxonomy_', '', $entity_type_id));
192 // By default the token type is the same as the entity type.
193 $entity_type->set('token_type', $entity_type_id);
199 && $entity_type->hasViewBuilderClass()
200 && ($canonical = $entity_type->getLinkTemplate('canonical'))
201 && !$entity_type->hasLinkTemplate('token-devel')) {
202 $entity_type->setLinkTemplate('token-devel', $canonical . '/devel/token');
208 * Implements hook_entity_view_modes_info().
212 * Implements hook_module_implements_alter().
214 * Adds missing token support for core modules.
216 function token_module_implements_alter(&$implementations, $hook) {
217 module_load_include('inc', 'token', 'token.tokens');
219 if ($hook == 'tokens' || $hook == 'token_info' || $hook == 'token_info_alter' || $hook == 'tokens_alter') {
220 foreach (_token_core_supported_modules() as $module) {
221 if (\Drupal::moduleHandler()->moduleExists($module) && function_exists($module . '_' . $hook)) {
222 $implementations[$module] = TRUE;
225 // Move token.module to get included first since it is responsible for
227 if (isset($implementations['token'])) {
228 unset($implementations['token']);
229 $implementations = array_merge(array('token' => 'tokens'), $implementations);
235 * Return the module responsible for a token.
237 * @param string $type
239 * @param string $name
243 * The value of $info['tokens'][$type][$name]['module'] from token info, or
244 * NULL if the value does not exist.
246 function _token_module($type, $name) {
247 $token_info = \Drupal::token()->getTokenInfo($type, $name);
248 return isset($token_info['module']) ? $token_info['module'] : NULL;
252 * Validate a form element that should have tokens in it.
254 * Form elements that want to add this validation should have the #token_types
259 * $form['my_node_text_element'] = array(
260 * '#type' => 'textfield',
261 * '#title' => t('Some text to token-ize that has a node context.'),
262 * '#default_value' => 'The title of this node is [node:title].',
263 * '#element_validate' => array('token_element_validate'),
264 * '#token_types' => array('node'),
265 * '#min_tokens' => 1,
266 * '#max_tokens' => 10,
270 function token_element_validate($element, FormStateInterface $form_state) {
271 $value = isset($element['#value']) ? $element['#value'] : $element['#default_value'];
273 if (!Unicode::strlen($value)) {
274 // Empty value needs no further validation since the element should depend
275 // on using the '#required' FAPI property.
279 $tokens = \Drupal::token()->scan($value);
280 $title = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];
282 // Validate if an element must have a minimum number of tokens.
283 if (isset($element['#min_tokens']) && count($tokens) < $element['#min_tokens']) {
284 $error = \Drupal::translation()->formatPlural($element['#min_tokens'], '%name must contain at least one token.', '%name must contain at least @count tokens.', array('%name' => $title));
285 $form_state->setError($element, $error);
288 // Validate if an element must have a maximum number of tokens.
289 if (isset($element['#max_tokens']) && count($tokens) > $element['#max_tokens']) {
290 $error = \Drupal::translation()->formatPlural($element['#max_tokens'], '%name must contain at most one token.', '%name must contain at most @count tokens.', array('%name' => $title));
291 $form_state->setError($element, $error);
294 // Check if the field defines specific token types.
295 if (isset($element['#token_types'])) {
296 $invalid_tokens = \Drupal::token()->getInvalidTokensByContext($tokens, $element['#token_types']);
297 if ($invalid_tokens) {
298 $form_state->setError($element, t('%name is using the following invalid tokens: @invalid-tokens.', array('%name' => $title, '@invalid-tokens' => implode(', ', $invalid_tokens))));
306 * Implements hook_form_FORM_ID_alter().
308 function token_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state) {
309 $field_config = $form_state->getFormObject()->getEntity();
310 $field_storage = $field_config->getFieldStorageDefinition();
311 if ($field_storage->isLocked()) {
314 $field_type = $field_storage->getType();
315 if (($field_type == 'file' || $field_type == 'image') && isset($form['settings']['file_directory'])) {
316 // GAH! We can only support global tokens in the upload file directory path.
317 $form['settings']['file_directory']['#element_validate'][] = 'token_element_validate';
318 // Date support needs to be implicitly added, as while technically it's not
319 // a global token, it is a not only used but is the default value.
320 // https://www.drupal.org/node/2642160
321 $form['settings']['file_directory'] += array('#token_types' => array('date'));
322 $form['settings']['file_directory']['#description'] .= ' ' . t('This field supports tokens.');
325 // Note that the description is tokenized via token_field_widget_form_alter().
326 $form['description']['#element_validate'][] = 'token_element_validate';
327 $form['description'] += array('#token_types' => array());
329 $form['token_tree'] = array(
330 '#theme' => 'token_tree_link',
331 '#token_types' => array(),
332 '#weight' => $form['description']['#weight'] + 0.5,
337 * Implements hook_form_BASE_FORM_ID_alter().
339 * Alters the configure action form to add token context validation and
340 * adds the token tree for a better token UI and selection.
342 function token_form_action_form_alter(&$form, $form_state) {
343 switch ($form['plugin']['#value']) {
344 case 'action_message_action':
345 case 'action_send_email_action':
346 case 'action_goto_action':
347 $form['token_tree'] = [
348 '#theme' => 'token_tree_link',
349 '#token_types' => 'all',
352 $form['actions']['#weight'] = 101;
353 // @todo Add token validation to the action fields that can use tokens.
359 * Implements hook_form_FORM_ID_alter().
361 * Alters the user e-mail fields to add token context validation and
362 * adds the token tree for a better token UI and selection.
364 function token_form_user_admin_settings_alter(&$form, FormStateInterface $form_state) {
365 $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].');
367 foreach (Element::children($form) as $key) {
368 $element = &$form[$key];
370 // Remove the crummy default token help text.
371 if (!empty($element['#description'])) {
372 $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']));
376 case 'email_admin_created':
377 case 'email_pending_approval':
378 case 'email_no_approval_required':
379 case 'email_password_reset':
380 case 'email_cancel_confirm':
381 // Do nothing, but allow execution to continue.
383 case 'email_activated':
384 case 'email_blocked':
385 case 'email_canceled':
386 // These fieldsets have their e-mail elements inside a 'settings'
387 // sub-element, so switch to that element instead.
388 $element = &$form[$key]['settings'];
394 foreach (Element::children($element) as $sub_key) {
395 if (!isset($element[$sub_key]['#type'])) {
398 elseif ($element[$sub_key]['#type'] == 'textfield' && substr($sub_key, -8) === '_subject') {
399 // Add validation to subject textfields.
400 $element[$sub_key]['#element_validate'][] = 'token_element_validate';
401 $element[$sub_key] += array('#token_types' => array('user'));
403 elseif ($element[$sub_key]['#type'] == 'textarea' && substr($sub_key, -5) === '_body') {
404 // Add validation to body textareas.
405 $element[$sub_key]['#element_validate'][] = 'token_element_validate';
406 $element[$sub_key] += array('#token_types' => array('user'));
411 // Add the token tree UI.
412 $form['email']['token_tree'] = array(
413 '#theme' => 'token_tree_link',
414 '#token_types' => array('user'),
415 '#show_restricted' => TRUE,
416 '#show_nested' => FALSE,
422 * Prepare a string for use as a valid token name.
425 * The token name to clean.
427 * The cleaned token name.
429 function token_clean_token_name($name) {
430 static $names = array();
432 if (!isset($names[$name])) {
433 $cleaned_name = strtr($name, array(' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => ''));
434 $cleaned_name = preg_replace('/[^\w\-]/i', '', $cleaned_name);
435 $cleaned_name = trim($cleaned_name, '-');
436 $names[$name] = $cleaned_name;
439 return $names[$name];
443 * Do not use this function yet. Its API has not been finalized.
445 function token_render_array(array $array, array $options = array()) {
448 /** @var \Drupal\Core\Render\RendererInterface $renderer */
449 $renderer = \Drupal::service('renderer');
451 foreach (token_element_children($array) as $key) {
452 $value = $array[$key];
453 $rendered[] = is_array($value) ? $renderer->renderPlain($value) : (string) $value;
455 $join = isset($options['join']) ? $options['join'] : ', ';
456 return implode($join, $rendered);
460 * Do not use this function yet. Its API has not been finalized.
462 function token_render_array_value($value, array $options = array()) {
463 /** @var \Drupal\Core\Render\RendererInterface $renderer */
464 $renderer = \Drupal::service('renderer');
466 $rendered = is_array($value) ? $renderer->renderPlain($value) : (string) $value;
471 * Copy of drupal_render_cache_get() that does not care about request method.
473 function token_render_cache_get($elements) {
474 if (!$cid = drupal_render_cid_create($elements)) {
477 $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
479 if (!empty($cid) && $cache = \Drupal::cache($bin)->get($cid)) {
480 // Add additional libraries, JavaScript, CSS and other data attached
482 if (isset($cache->data['#attached'])) {
483 drupal_process_attached($cache->data);
485 // Return the rendered output.
486 return $cache->data['#markup'];
492 * Coyp of drupal_render_cache_set() that does not care about request method.
494 function token_render_cache_set(&$markup, $elements) {
495 // This should only run of drupal_render_cache_set() did not.
496 if (in_array(\Drupal::request()->server->get('REQUEST_METHOD'), array('GET', 'HEAD'))) {
500 $original_method = \Drupal::request()->server->get('REQUEST_METHOD');
501 \Drupal::request()->server->set('REQUEST_METHOD', 'GET');
502 drupal_render_cache_set($markup, $elements);
503 \Drupal::request()->server->set('REQUEST_METHOD', $original_method);
507 * Loads menu link titles for all purents of a menu link plugin ID.
509 * @param string $plugin_id
510 * The menu link plugin ID.
511 * @param string $langcode
515 * List of menu link parent titles.
517 function token_menu_link_load_all_parents($plugin_id, $langcode) {
518 $cache = &drupal_static(__FUNCTION__, array());
520 if (!isset($cache[$plugin_id][$langcode])) {
521 $cache[$plugin_id][$langcode] = array();
522 /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
523 $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
524 $parent_ids = $menu_link_manager->getParentIds($plugin_id);
525 // Remove the current plugin ID from the parents.
526 unset($parent_ids[$plugin_id]);
527 foreach ($parent_ids as $parent_id) {
528 $parent = $menu_link_manager->createInstance($parent_id);
529 $cache[$plugin_id][$langcode] = array($parent_id => token_menu_link_translated_title($parent, $langcode)) + $cache[$plugin_id][$langcode];
533 return $cache[$plugin_id][$langcode];
537 * Returns the translated link of a menu title.
539 * If the underlying entity is a content menu item, load it to get the
540 * translated menu item title.
542 * @todo Remove this when there is a better way to get a translated menu
543 * item title in core: https://www.drupal.org/node/2795143
545 * @param \Drupal\Core\Menu\MenuLinkInterface $menu_link
547 * @param string|null $langcode
548 * (optional) The langcode, defaults to the current language.
551 * The menu link title.
553 function token_menu_link_translated_title(MenuLinkInterface $menu_link, $langcode = NULL) {
554 $metadata = $menu_link->getMetaData();
555 if (isset($metadata['entity_id']) && $menu_link->getProvider() == 'menu_link_content') {
556 /** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
557 $entity = \Drupal::entityTypeManager()->getStorage('menu_link_content')->load($metadata['entity_id']);
558 $entity = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode);
559 return $entity->getTitle();
561 return $menu_link->getTitle();
565 * Loads all the parents of the term in the specified language.
569 * @param string $langcode
573 * The term parents collection.
575 function token_taxonomy_term_load_all_parents($tid, $langcode) {
576 $cache = &drupal_static(__FUNCTION__, array());
578 if (!is_numeric($tid)) {
582 if (!isset($cache[$langcode][$tid])) {
583 $cache[$langcode][$tid] = array();
584 /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
585 $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
586 $parents = $term_storage->loadAllParents($tid);
587 // Remove this term from the array.
588 array_shift($parents);
589 $parents = array_reverse($parents);
590 foreach ($parents as $term) {
591 $translation = \Drupal::service('entity.repository')->getTranslationFromContext($term, $langcode);
592 $cache[$langcode][$tid][$term->id()] = $translation->label();
596 return $cache[$langcode][$tid];
599 function token_element_children(&$elements, $sort = FALSE) {
600 // Do not attempt to sort elements which have already been sorted.
601 $sort = isset($elements['#sorted']) ? !$elements['#sorted'] : $sort;
603 // Filter out properties from the element, leaving only children.
606 foreach ($elements as $key => $value) {
607 if ($key === '' || $key[0] !== '#') {
608 $children[$key] = $value;
609 if (is_array($value) && isset($value['#weight'])) {
614 // Sort the children if necessary.
615 if ($sort && $sortable) {
616 uasort($children, 'Drupal\Component\Utility\SortArray::sortByWeightProperty');
617 // Put the sorted children back into $elements in the correct order, to
618 // preserve sorting if the same element is passed through
619 // element_children() twice.
620 foreach ($children as $key => $child) {
621 unset($elements[$key]);
622 $elements[$key] = $child;
624 $elements['#sorted'] = TRUE;
627 return array_keys($children);
631 * Loads all the parents of the book page.
634 * The book data. The 'nid' key points to the current page of the book.
635 * The 'p1' ... 'p9' keys point to parents of the page, if they exist, with 'p1'
636 * pointing to the book itself and the last defined pX to the current page.
639 * List of node titles of the book parents.
641 function token_book_load_all_parents(array $book) {
642 $cache = &drupal_static(__FUNCTION__, array());
644 if (empty($book['nid'])) {
649 if (!isset($cache[$nid])) {
650 $cache[$nid] = array();
652 while ($book["p$i"] != $nid) {
653 $cache[$nid][] = Node::load($book["p$i"])->getTitle();
662 * Implements hook_entity_base_field_info().
664 function token_entity_base_field_info(EntityTypeInterface $entity_type) {
665 // We add a psuedo entity-reference field to track the menu entry created
666 // from the node add/edit form so that tokens generated at that time that
667 // reference the menu link can access the yet to be saved menu link.
668 // @todo Revisit when https://www.drupal.org/node/2315773 is resolved.
669 if ($entity_type->id() === 'node' && \Drupal::moduleHandler()->moduleExists('menu_ui')) {
670 $fields['menu_link'] = BaseFieldDefinition::create('entity_reference')
671 ->setLabel(t('Menu link'))
672 ->setDescription(t('Computed menu link for the node (only available during node saving).'))
673 ->setRevisionable(TRUE)
674 ->setSetting('target_type', 'menu_link_content')
675 ->setTranslatable(TRUE)
676 ->setDisplayOptions('view', array(
681 ->setDisplayOptions('form', array(
691 * Implements hook_form_BASE_FORM_ID_alter() for node_form.
693 * Populates menu_link field on nodes from the menu item on unsaved nodes.
695 * @see menu_ui_form_node_form_submit()
696 * @see token_entity_base_field_info()
698 function token_form_node_form_alter(&$form, FormStateInterface $form_state) {
699 if (!\Drupal::moduleHandler()->moduleExists('menu_ui')) {
702 /** @var \Drupal\node\NodeForm $form_object */
703 if (!\Drupal::currentUser()->hasPermission('administer menu')) {
704 // We're only interested in when the node is unsaved and the editor has
705 // permission to create new menu links.
708 $form['#entity_builders'][] = 'token_node_menu_link_submit';
714 function token_node_menu_link_submit($entity_type, NodeInterface $node, &$form, FormStateInterface $form_state) {
715 // Entity builders run twice, once during validation and again during
716 // submission, so we only run this code after validation has been performed.
717 if (!$form_state->isValueEmpty('menu') && $form_state->getTemporaryValue('entity_validated')) {
718 $values = $form_state->getValue('menu');
719 if (!empty($values['enabled']) && trim($values['title'])) {
720 if (!empty($values['menu_parent'])) {
721 list($menu_name, $parent) = explode(':', $values['menu_parent'], 2);
722 $values['menu_name'] = $menu_name;
723 $values['parent'] = $parent;
725 // Construct an unsaved entity.
726 if ($entity_id = $form_state->getValue(['menu', 'entity_id'])) {
727 // Use the existing menu_link_content entity.
728 $entity = MenuLinkContent::load($entity_id);
729 // If the loaded MenuLinkContent doesn't have a translation for the
730 // Node's active langcode, create a new translation.
731 if ($entity->isTranslatable()) {
732 if (!$entity->hasTranslation($node->language()->getId())) {
733 $entity = $entity->addTranslation($node->language()->getId(), $entity->toArray());
736 $entity = $entity->getTranslation($node->language()->getId());
741 if ($node->isNew()) {
742 // Create a new menu_link_content entity.
743 $entity = MenuLinkContent::create(array(
744 // Lets just reference the UUID for now, the link is not important for
746 'link' => ['uri' => 'internal:/node/' . $node->uuid()],
747 'langcode' => $node->language()->getId(),
751 // Create a new menu_link_content entity.
752 $entity = MenuLinkContent::create(array(
753 'link' => ['uri' => 'entity:node/' . $node->id()],
754 'langcode' => $node->language()->getId(),
758 $entity->title->value = trim($values['title']);
759 $entity->description->value = trim($values['description']);
760 $entity->menu_name->value = $values['menu_name'];
761 $entity->parent->value = $values['parent'];
762 $entity->weight->value = isset($values['weight']) ? $values['weight'] : 0;
764 $node->menu_link = $entity;
765 // Leave this for _menu_ui_node_save() to pick up so we don't end up with
766 // duplicate menu-links.
767 $form_state->setValue(['menu', 'entity_id'], $entity->id());
773 * Implements hook_ENTITY_TYPE_insert for node entities.
775 function token_node_insert(NodeInterface $node) {
776 if ($node->hasField('menu_link') && $menu_link = $node->menu_link->entity) {
777 // Update the menu-link to point to the now saved node.
778 $menu_link->link = 'entity:node/' . $node->id();
784 * Implements hook_ENTITY_TYPE_presave() for menu_link_content.
786 function token_menu_link_content_presave(MenuLinkContentInterface $menu_link_content) {
787 drupal_static_reset('token_menu_link_load_all_parents');