8

In my website, users can collect some 'points' over time. I have a top-bar menu with some links (e.g. "home", "profile", "logout"). I want to alter the menu item "profile" and add the number points owned by the current connected user.

I tried several hook, and I almost succeeded in getting what I wanted with hook_link_alter().

function mycustommodule_link_alter(&$variables) {
  if ($variables['text'] == "profile") {
    // Do some work.
    $variables['text'] = $variables['text'] . " (you have $nb_points points)";
  }
}

I cleared the cache (with drush cr), and the menu item shows the value I want. But, if the value changes for some reason, it shows the old value. I must clear the cache all the times to update its value.

How can I avoid cleaning the cache to update the menu title all the times the points gained from the user change?

5
  • It sounds like you need custom plugin for this. Interesting question. Generally you should avoid implementing any old school hooks in D8. The question is how to alter menu title via custom plugin.
    – Codium
    Oct 10, 2016 at 9:41
  • Is your menu in a block ? If that's the case maybe try to set the cache to 0 for that block
    – pbonnefoi
    Oct 11, 2016 at 14:57
  • its a block created from the administration interface. I didn't access to cache configuration :) Oct 11, 2016 at 19:08
  • I suggest you start with drupal.org/docs/8/api/menu-api , the '#cache' => ['max-age' => 0], is not a solution. you need to create a dynamic menu link. Oct 26, 2016 at 14:37
  • "the menu item shows the value I want" Have you checked the menu with other user account? I suspect it is showing the same number of points for all users.
    – ya.teck
    Nov 2, 2016 at 17:30

3 Answers 3

10

I suggest implementing a custom menu link plugin. The code below assumes your module name is example.

<?php

namespace Drupal\example\Plugin\Menu;

use Drupal\Core\Database\Connection;
use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Menu\StaticMenuLinkOverridesInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * A menu link that displays number of points.
 */
class ExampleMenuLink extends MenuLinkDefault {

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $dbConnection;

  /**
   * Constructs a new points menu link.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $static_override
   *   The static override storage.
   * @param \Drupal\Core\Database\Connection $db_connection
   *   The database connection.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, StaticMenuLinkOverridesInterface $static_override, Connection $db_connection) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $static_override);
    $this->dbConnection = $db_connection;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('menu_link.static.overrides'),
      $container->get('database')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getTitle() {
    $count = $this->dbConnection->query('SELECT COUNT(*) FROM {example_points}')->fetchField();
    return $this->t('You have (@count) points', ['@count' => $count]);
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    // Invalidate these tags when number of points is changed.
    return ['example.points_count'];
  }

}

If you don't want to inject the database service the class would become much simpler.

<?php

namespace Drupal\example\Plugin\Menu;

use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Menu\StaticMenuLinkOverridesInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * A menu link that displays number of points.
 */
class ExampleMenuLink extends MenuLinkDefault {

  /**
   * {@inheritdoc}
   */
  public function getTitle() {
    $count = \Drupal::database()->query('SELECT COUNT(*) FROM {example_points}')->fetchField();
    return $this->t('You have (@count) points', ['@count' => $count]);
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    // Invalidate these tags when number of points is changed.
    return ['example.points_count'];
  }

}

Next you need to put the link definition into example.links.menu.yml file.

example.user_points:
  route_name: <front>
  menu_name: main
  class: Drupal\example\Plugin\Menu\ExampleMenuLink
  weight: 30

The caching problem

Whenever the number of points is changed the menu link cache should be invalidated as follows.

 \Drupal::service('cache_tags.invalidator')->invalidateTags(['example.points_count']);

You need to find out the right place for this. If the points managed by contributed module check the module API and pick up an appropriate hook (hook_points_insert(), hook_points_delete() and so on).

Since the number of points is calculated for each user account individually you may consider using per account cache tags (something like ['example.points_count.' . $uid]). Therefore the cache will be preserved for users with unchanged points.


To generate code for the Menu link plugin I used Drupal Code Generator.

3
  • This answer is spot on (and more helpful than the current official documentation. However, the yaml file should be named example.links.menu.yml, no example.menu-links.yml. After that, clear the cache and your menu item should appear.
    – Sut3kh
    May 31, 2017 at 8:27
  • Is this possible to override also the getRouteParameters() method? If no, how one can alter the route_parameters to be dynamic? In my case I have {user} in routing and menu link should provide the user ID dynamically as user ID of current user.
    – Igal
    Jun 3, 2017 at 16:30
  • @Igal think it is possible.
    – ya.teck
    Jun 4, 2017 at 3:59
2

I faced the same problem. The menu items are cached, so it always shows the old value until you clear the cache. Alternative way is using hook_page_attachments(), attach points to drupalSettings.YOUR_MODULE_OR_THEME.YOUR_VARIABLE, and access them in JavaScript, and render in the browser.

Normal way is disabling cache for pages whenever the "profile" menu displays at site performance.

0

Disable the cache for that menu in hook_preprocess_menu() by setting $variables['#cache']['max-age'] to 0.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.