3

I would like to set a user cache context to a Views, but I don't know where to do this (in what hook ?). In the configuration of the View, the cache is set "Tag based"

Advanced settings of the Views

Each user have a specific property, and I want the Views to deal with it, without totally disable the cache of the Views.

I already check this hook without success :

hook_entity_build_defaults_alter hook_entity_views_data_alter hook_views_pre_views

In each hook I have changed the $view->element[#cache][contexts] array.


I give some clarifications to my problem.

I've a "Product" content type. This content type has a reference field to a Taxonomy called "Family product" and a "Public price" number field. My goal is to display a computed price the for User called "Your price". "Your price" is not a field, because it's a computed value. To compute this value, I have another content-type called "Discount grid". With a form_alter on this content type, I list each term of the "Family product" and associate them with a textfield who take a percent of the discount value the administrator grants to this grid.

To finish, the User entity has a reference field to a "Discount grid" content. The association between the User and the Discount grid allow me to compute the "Your price" according to the family who belongs the "Product".

I hope I didn't loose you, not easy to explain.

So, I try many things since I've open this question, without success. To display my computed value with Views, I've created a custom Views field who display the computed "Your price" value by taking the "Discount grid" of the current user (the query() function is empty to prevent Views to search the field value on the database). It's working like a charm.

/**
 * @ingroup views_field_handlers
 * @ViewsField("node_costumer_price")
 */
class NodeCostumerPrice extends FieldPluginBase {

    /**
     * @{inheritdoc}
     */
    public function query() {
        // Leave empty to avoid a query on this field.
    }

    /**
     * @{inheritdoc}
     * @param \Drupal\views\ResultRow $values
     */
    public function render(ResultRow $values) {

        //Called a custom service who return the current User Discount grid
        $grid = \Drupal::service('discount.service')->getGrid();
        $node = $values->_entity;
        $price = "";

        $family = $node->get('field_family')->getValue();
        $publicprice = $node->get('field_public_price')->getValue();
        /* doing some calculation here */
        return $this->t(number_format((float) $price, 2, ',', '') . ' €');
    }
}

The problem is the cache. The computed value is always displaying the same price between two User with a different Discount grid.

I've created a custom cache context "discount". This context is matched on the node ID of the Discount Grid of the current User. This context work very well on a simple render array (tested returning a render array on a custom controller).

#modules/custom/discount/src/Context/DiscountCacheContext.php

/**
 * Defines the DiscountCacheContext service, for "per discount grid" caching.
 * Cache context ID: 'discount'.
 */
class DiscountCacheContext implements CacheContextInterface {
    /**
     * The account object.
     *
     * @var \Drupal\Core\Session\AccountInterface
     */
    protected $user;

    /**
     * Constructs a new UserCacheContextBase class.
     *
     * @param \Drupal\Core\Session\AccountInterface $user
     *   The current user.
     */
    public function __construct(AccountInterface $user) {
        $this->user = $user;
    }

    /**
     * {@inheritdoc}
     */
    public static function getLabel() {
        return t('Discount');
    }

    /**
     * {@inheritdoc}
     */
    public function getContext() {
        $user = User::load($this->user->id());
        return empty($user->get('field_auto_part_discount_grid')->getValue()) ? 0 :
          $user->get('field_auto_part_discount_grid')->getValue()['0']['target_id'];
    }

    /**
     * {@inheritdoc}
     */
    public function getCacheableMetadata() {
        return new CacheableMetadata();
    }
}

Now I'm searching how to "graft" this custom context cache on the Views. The last thing I've tried is to create a custom Views filter handler for injecting the cache context in it, without success. (by trying to reproduce the core/modules/user/src/Plugin/views/filter/Current class and adapt it with my custom cache context)

I hope be clear enough this time. I try all this week to find a solution without disable the Views caching (render time /2).

1
  • You need to clarify what exactly you want to do, should it vary by the currently logged in user and some of his information, or by the data that is displayed. Share the code you have and what kind of cache context you tried to use
    – Berdir
    Commented Nov 6, 2016 at 14:06

3 Answers 3

12
+50

Since you want the view to be custom to each user, based on that user's field_auto_part_discount_grid, you can just add the user tag to the view's cache array, assuming it isn't already there. You could also create a custom cache tag based on the price.

function mymodule_views_pre_view(ViewExecutable $view, $display_id, array &$args) {

  if ($view->id() == 'user_price') {
    $user_id = args[0];
    $user_price = 12345; // replace with logic to get price
    $view->element['#cache']['tags']['user:' . $user_id, 'discount:' . $user_price];
  }
}

Also, any cache tags from within the render array of the view will bubble up to the view, so you can add the user cache tag in NodeCostumerPrice's render function:

Instead of

return $this->t(number_format((float) $price, 2, ',', '') . ' €');

do this:

$user_price = $this->t(number_format((float) $price, 2, ',', '') . ' €');

return [
  '#markup' => $user_price,
  '#cache' => ['tags' => ['user:'.$this->user->id(), 'discount:' . $user_price]]
];

This will then bubble up and be added to the views cache tags in the render cache, and will invalidate the cache when the user is updated.

You can add a custom cache tag instead of the user, if you want use a custom tag, based on your custom price.

Then in hook_entity_update, you can invalidate the cache tag based on whatever logic you like. For example, if a user is being saved, you could invalidate your custom cache tag 'discount':

/**
 * Implements hook_entity_update().
 */
function mymodule_entity_update($entity) {
  if ($entity instanceof User && $entity->field_public_price->isEmpty() === FALSE) {
    $user_price = 12345; // replace with logic to get price.
    Cache::invalidateTags(array('discount:' . $user_price));
  }
}
2
  • I check my own answer as accepted because it describe the solution a I was looking for. But or course you totally deserve the bounty points. Thanks you so much! (15h left until I can give you the bounty points)
    – Gaius
    Commented Nov 18, 2016 at 21:08
  • This: $view->element['#cache']['tags']['user:' . $user_id, 'discount:' . $user_price]; is not proper PHP as it isn't assigning anything. Likely was meant to be: $view->element['#cache']['tags'][] = 'user:' . $user_id, 'discount:' . $user_price;
    – liquidcms
    Commented Jan 11, 2022 at 14:48
6

You can't configure which cache contexts or cache tags views is going to use. It gets this by calculated the cache tags and contexts of all, fields, filters, displays, contexts, relationships etc used in the view.

If you enabled tag caching and used current user as context somewhere, (so you actually print something that varies per user) you should get this automatically. Otherwise you should create a handler to output the per user data, which should define the proper cache context using ::getCacheContexts, then the view using this will automatically get the user context.

The clever thing behind Drupal's caching, is that it merges items, to parts the are general could use a common cache. Drupal 8 also has some clever ideas, with using placeholders in caches, so it can cache rendered html of a page with placeholder for username and simply swap username based on logged in user. It might be worth looking into this feature.

1
  • 1
    As the other answers illustrate, you can in fact do this in hook_views_pre_view or hook_views_query_alter. And if you're modifying the view in those hooks, you might need to modify the cache contexts & tags there too. A handler might be a fundamentally better solution though.
    – Jonathan
    Commented Feb 22, 2017 at 18:53
4

Thank you, @oknate, I found the exact solution I needed based on your answer.

You advised me to use the user ID to alter the cache tag. This solution isn't ideal because each time that a different user refreshes the content, it invalidates the cache and it is necessary to regenerate it (which requires a lot of processing).

But the next part of your answer gave me the idea to transform the user price as a render array (didn't think about it before). To this render array, I passed my custom 'discount' cache context. The view's result now has a cache version for each discount grid. With this context, if two user share the same grid, the same cached results will hit for both of the users, witch is my very goal !

       $user_price = number_format((float) $price, 2, ',', '') . ' €';

        return [
          '#markup' => $user_price,
          '#cache' => ['contexts' => ['discount']]
        ];

By the way, I remove the t() function, completely useless here (some rests of a tutorial found on the web).

1
  • 2
    It's better to not overwrite the existing cache contexts but merge them, so that all cache contexts that might originally be present on the view are preserved.
    – pfrenssen
    Commented Dec 16, 2016 at 16:21

Your Answer

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

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