4

Using a deriver, I'm able to produce local tasks for each entity of my custom configuration entity type. So if I have two configuration entity labeled "Foo" and "Bar", I can have similarly labeled tabs on a report page.

However, when adding or removing one of my configuration entity, the local tasks are not updated (ie. the tab for the deleted entity is still visible). This is not a surprise as these things get cached.

I've been able to fix the issues using implementations of hook_ENTITY_TYPE_insert() and hook_ENTITY_TYPE_delete() calling drupal_flush_all_caches(). But clearing all caches in order to only get the tabs on a single page updated seems a bit overkill. I already tried to call both menu_cache_clear_all() and Drupal::cache('discovery')->invalidateAll() without success

The tabs are build using the following code:

MODULE.tasks.yml:

MODULE.settings.webhooks_list:
  deriver: 'Drupal\MODULE\Plugin\Menu\LocalTask\ReportLocalTasks'
  base_route: MODULE.settings.list

src/Plugin/Menu/LocalTasks/ReportLocalTasks.php:

namespace Drupal\MODULE\Plugin\Menu\LocalTask;


use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\MODULE\CustomEntityTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ReportLocalTasks extends DeriverBase implements ContainerDeriverInterface {
  use StringTranslationTrait;

  protected $entityManager;

  /**
   * Constructor.
   *
   * @param $entityManager
   * @param $stringTranslation;
   */
  public function __construct(EntityTypeManagerInterface $entityManager, TranslationInterface $stringTranslation) {
    $this->entityManager = $entityManager;
    $this->stringTranslation = $stringTranslation;
  }


  public static function create(ContainerInterface $container, $base_plugin_id) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('string_translation')
    );
  }


  public function getDerivativeDefinitions($base_plugin_definition) {
    $this->derivatives = [];

    /** @var EntityStorageInterface $entityStorage */
    $entityStorage = $this->entityManager->getStorage('custom_type');

    /** @var CustomEntityTypeInterface $connection */
    foreach ($entityStorage->loadMultiple() as $entity) {
      $this->derivatives["MODULE.settings.list.{$entity->id()}"] = [
        'title' => $entity->label(),
        'route_name' => 'MODULE_ecom.settings.list',
        'route_parameters' => [
          'connection' => $entity->id(),
        ],
      ];
    }

    foreach ($this->derivatives as &$entry) {
      $entry += $base_plugin_definition;
    }

    return $this->derivatives;
  }
}

In order to have up to date tabs when adding or removing custom entity, I'm using the following code. What I'm trying to do is to have something more specific than the call the drupal_flush_all_caches().

MODULE.module

/**
 * Implements hook_ENTITY_TYPE_insert();
 */
function MODULE_CUSTOM_ENTITY_ETYPE_insert(\Drupal\MODULE\CustomEntityTypeInterface $connection) {
  drupal_flush_all_caches();
}

/**
 * Implements hook_ENTITY_TYPE_delete();
 */
function lMODULE_CUSTOM_ENTITY_ETYPE_delete(\Drupal\MODULE\CustomEntityTypeInterface $connection) {
  drupal_flush_all_caches();
}
2
  • Probably you could invalidate only the rendered cache instead of all cache. Something like \Drupal\Core\Cache\Cache::invalidateTags(['rendered']); But with this vague context is hard to answer. Please can you share you code?
    – marco
    Commented Aug 31, 2016 at 14:23
  • 1
    Core issue to fix this is here drupal.org/project/drupal/issues/3001284 Commented Dec 3, 2018 at 1:52

4 Answers 4

5
+100

First of all it might be that you found a bug in core (a caching issue), so it would be great if you could report it to the Drupal core issue queue as well. There is extensive test coverage for local tasks, but it might be that there is none for adding new entities and that those in core just work due to side effects.

To solve your immediate issue I would try to invalidate just the render cache of the local task blocks.

To find out the cacheability metadata of the local tasks the easiest is to install the renderviz module:

https://www.drupal.org/project/renderviz/

and apply the patch from this comment

https://www.drupal.org/node/2640280#comment-11286381

Once you have that installed and you go to the page with your local tasks, you will see a list in the JS console of all cache tags present.

For the bartik theme you will find (e.g. tested with core search pages, which have similar code to yours):

config:block.block.bartik_local_tasks

If you put into the console:

  renderviz('tags', 'config:block.block.bartik_local_tasks');

Then you will see that indeed the primary local tasks will be highlighted.

So in your hook_entity_insert() / hook_entity_update(), you can just do:

 Drupal\Core\Cache\Cache::invalidateTags('config:block.block.bartik_local_tasks');

And that will invalidate just the local tasks block and solve your issue for now.

What should have happened however is that the local tasks block should show both the cache tags for the list [entity-type] and the entities itself. (like a normal listing of entities)

e.g. if you want to change the title of any of your entities, core should based on the deriver update the local tasks block.

And that we don't do that is likely a core bug in regards to cacheability metadata, so please file it. Unless I miss something and there is some special code that makes this work for core's derivers out-of-the box.

Edit:

We correctly add the cache tags of the config entities, however there is no 'list' cache tag, which means that whenever you add or delete a config entity the local tasks don't get invalidated.

As you have a plugin, you can likely just add your own custom cache tag to the plugin definition.

e.g.

$base_plugin_definition['cache_tags'][] = 'local-tasks-my-entity-type-list-cache-tag';

Then just invalidate that list cache tag in your insert / update.

But that definitely is also an issue in Drupal core as I can reproduce it purely with core search pages.

I missed in your question at first that caches fail to get invalidated only when adding or deleting new entities.

That however was the key clue.

Edit 2:

Core should be invalidating list cache tags automatically, so the easiest is to just use the pre-defined list cache tags.

This is the diff that fixes the bug for me in core:

diff --git a/core/modules/search/src/Plugin/Derivative/SearchLocalTask.php b/core/modules/search/src/Plugin/Derivative/SearchLocal
--- a/core/modules/search/src/Plugin/Derivative/SearchLocalTask.php
+++ b/core/modules/search/src/Plugin/Derivative/SearchLocalTask.php
@@ -52,7 +52,9 @@ public function getDerivativeDefinitions($base_plugin_definition) {
           'route_name' => 'search.view_' . $entity_id,
           'base_route' => 'search.plugins:' . $default,
           'weight' => $entity->getWeight(),
 -        );
 +        ) + $base_plugin_definition;
 +
 +        $this->derivatives[$entity_id] += ['cache_tags' => []];
 +        $this->derivatives[$entity_id]['cache_tags'] += $entity->getEntityType()->getListCacheTags();
       }
     }
     return $this->derivatives;

That fixes the bug for me for core search pages.

7
  • I don't think that would be a core bug. Core does not known about my custom deriver. It cannot known it has to clear/cache something related to local tasks when a custom config entity is inserted/delete. I don't have to test ATM, but I'm pretty sure this is not a render cache issue. My current assumption is that multiple cache are involved, the render cache may be one but I guess plugin and menu cache are involved. Commented Sep 8, 2016 at 20:45
  • The core bug / limitation here is that you have no way to tell core the cacheability metadata that you need. It should really be associated with the LocalTask item itself. e.g. you want to show the list and entity cache tags when displaying a list of entities. I am just unclear how e.g. search pages get around that issue (if they do). And yes it might well be that the discovery cache needs to be cleared as well.
    – LionsAd
    Commented Sep 8, 2016 at 21:56
  • Hmmm, maybe you need to add "class: \Drupal\Core\Menu\LocalTaskDefault" to your *.task.yml?
    – LionsAd
    Commented Sep 8, 2016 at 22:13
  • It is a core bug and a very nasty one for that. I can reproduce it with normal core search pages starting from the third search page. I get the curious issue that one one page I have 2 tabs on the new page I have three tabs. Clearly cached by page_cache and dynamic_page_cache.
    – LionsAd
    Commented Sep 8, 2016 at 22:37
  • I edited my answer to provide the final solution, still need to file a core issue though.
    – LionsAd
    Commented Sep 9, 2016 at 11:42
1

Based on LionsAd answer, there is no need to manually clear any cache in implementations of hook_ENTITY_TYPE_insert and hook_ENTITY_TYPE_delete. Instead, the plugin derivative should be marked with the entity type list cache tags, so Drupal can clear the appropriate caches when needed. The code for my derivative now looks like this:

namespace Drupal\MODULE\Plugin\Menu\LocalTask;


use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\MODULE\CustomEntityTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ReportLocalTasks extends DeriverBase implements ContainerDeriverInterface {
  use StringTranslationTrait;

  protected $entityManager;

  /**
   * Constructor.
   *
   * @param $entityManager
   * @param $stringTranslation;
   */
  public function __construct(EntityTypeManagerInterface $entityManager, TranslationInterface $stringTranslation) {
    $this->entityManager = $entityManager;
    $this->stringTranslation = $stringTranslation;
  }


  public static function create(ContainerInterface $container, $base_plugin_id) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('string_translation')
    );
  }


  public function getDerivativeDefinitions($base_plugin_definition) {
    $this->derivatives = [];

    /** @var EntityStorageInterface $entityStorage */
    $entityStorage = $this->entityManager->getStorage('custom_type');

    $entityType = $this-> entityManager->getDefinition('lightspeed_ecom_connection');

    /** @var CustomEntityTypeInterface $connection */
    foreach ($entityStorage->loadMultiple() as $entity) {
      $this->derivatives["MODULE.settings.list.{$entity->id()}"] = [
        'title' => $entity->label(),
        'route_name' => 'MODULE_ecom.settings.list',
        'route_parameters' => [
          'connection' => $entity->id(),
        ],
      ];
    }

    foreach ($this->derivatives as &$entry) {
      $entry += $base_plugin_definition;
      $entry  += ['cache_tags' => []];
      $entry['cache_tags'] += $entityType->getListCacheTags();
    }

    return $this->derivatives;
  }
}
0

Knowing that drupal_flush_all_caches() solves your problem, all you need to do is to execute a subset of that function's code. The docs for drupal_flush_all_caches() is well documented so you can create a variant of that function that is specific for your needs.

For example:

function _MODULE_flush_partial_cache() {

  // Empty all cache bins & let modules take action on it
  // It possible that you might get by with emptying a subset of cache bins
  // (e.g. cache.menu & cache.entity?) but lets knock them all out.
  $module_handler = \Drupal::moduleHandler();
  $module_handler->invokeAll('cache_flush');
  foreach (Cache::getBins() as $service_id => $cache_backend) {
    $cache_backend->deleteAll();
  }

  // Ignore CSS, JS, static, Twig, module, & theme 
  // rebuilding that's not needed in this context.

  // Obviously, your routes need to be rebuilt.
  \Drupal::service('router.builder')->rebuild();
}

/**
 * Implements hook_ENTITY_TYPE_insert();
 */
function MODULE_CUSTOM_ENTITY_ETYPE_insert(\Drupal\MODULE\CustomEntityTypeInterface $connection) {
  _MODULE_flush_partial_cache();
}

/**
 * Implements hook_ENTITY_TYPE_delete();
 */
function MODULE_CUSTOM_ENTITY_ETYPE_delete(\Drupal\MODULE\CustomEntityTypeInterface $connection) {
  _MODULE_flush_partial_cache();
}
0

I found a simple solution for this case:

  1. there is a local task defined in my_module.links.task.yml
  2. Route has _custom_access - visibility of Tab depends on Node-Values
  3. I add HOOK_menu_local_tasks_alter with this code
    $data['cacheability']->addCacheTags($node->getCacheTags());

Now the local-Tasks will be build again if node is saved.

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.