3

I try to create some local second level tasks depending on some settings for a custom entity. But unfortunately sometimes these local tasks do not appear - they appear again when rebuilding caching and first level tabs appear but not these dynamically created second level tabs.

What I have done (the entity type name is "parliament_period"):

In mymodule.links.task.yml

entity.parliament_period.canonical:
  route_name: entity.parliament_period.canonical
  base_route: entity.parliament_period.canonical
  title: 'Overview'

entity.parliament_period.edit_form:
  route_name: entity.parliament_period.edit_form
  base_route: entity.parliament_period.canonical
  title: 'Edit'

entity.parliament_period.local_tasks:
  deriver: 'Drupal\mymodule\Plugin\Derivative\ParliamentPeriodDynamicLocalTasks'
  weight: 100

The class looks the following:

class ParliamentPeriodDynamicLocalTasks extends DeriverBase  implements ContainerDeriverInterface {

  /**
   * @var \Drupal\Core\Routing\CurrentRouteMatch
   */
  protected $currentRouteMatch;


  /**
   * @var ParliamentPeriod|null
   */
  protected $parliamentPeriod;

  /**
   * DynamicLocalTasks constructor.
   *
   * @param \Drupal\Core\Routing\CurrentRouteMatch $current_route_match
   */
  public function __construct(CurrentRouteMatch $current_route_match) {
    $this->currentRouteMatch = $current_route_match;
    $this->parliamentPeriod = $this->currentRouteMatch->getParameter('parliament_period');
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, $base_plugin_id) {
    return new static(
      $container->get('current_route_match')
    );
  }



  /**
   * {@inheritdoc}
   */
  public function getDerivativeDefinitions($base_plugin_definition) {
    if (!$this->parliamentPeriod instanceof ParliamentPeriod) {
      return [];
    }

    // Implement dynamic logic to provide values for the same keys as in example.links.task.yml.
    $this->derivatives['entity.parliament_period.default']['title'] = 'Details';
    $this->derivatives['entity.parliament_period.default']['provider'] = $base_plugin_definition["provider"];
    $this->derivatives['entity.parliament_period.default']['route_name'] = 'entity.parliament_period.canonical';
    $this->derivatives['entity.parliament_period.default']['parent_id'] = "entity.parliament_period.canonical";


    if ($this->parliamentPeriod->isElection()) {
      $this->derivatives['entity.parliament_period.candidacies']['title'] = "Candidacies";
      $this->derivatives['entity.parliament_period.candidacies']['provider'] = $base_plugin_definition["provider"];
      $this->derivatives['entity.parliament_period.candidacies']['route_name'] = 'entity.parliament_period.candidacies';
      $this->derivatives['entity.parliament_period.candidacies']['parent_id'] = "entity.parliament_period.canonical";


      $this->derivatives['entity.parliament_period.constituencies']['title'] = "Constituencies";
      $this->derivatives['entity.parliament_period.constituencies']['provider'] = $base_plugin_definition["provider"];
      $this->derivatives['entity.parliament_period.constituencies']['route_name'] = 'entity.parliament_period.constituencies';
      $this->derivatives['entity.parliament_period.constituencies']['parent_id'] = "entity.parliament_period.canonical";
    }

    if ($this->parliamentPeriod->isLegislature()) {
      $this->derivatives['entity.parliament_period.committees']['title'] = "Committees";
      $this->derivatives['entity.parliament_period.committees']['provider'] = $base_plugin_definition["provider"];
      $this->derivatives['entity.parliament_period.committees']['route_name'] = 'entity.parliament_period.committees';
      $this->derivatives['entity.parliament_period.committees']['parent_id'] = "entity.parliament_period.canonical";

      $this->derivatives['entity.parliament_period.committees']['title'] = "Mandates";
      $this->derivatives['entity.parliament_period.committees']['provider'] = $base_plugin_definition["provider"];
      $this->derivatives['entity.parliament_period.committees']['route_name'] = 'entity.parliament_period.mandates';
      $this->derivatives['entity.parliament_period.committees']['parent_id'] = "entity.parliament_period.canonical";
    }


    return $this->derivatives;
  }

}

3
  • 4
    You can't make derivatives dynamic for each request, see drupal.stackexchange.com/questions/235402/…. Here it is solved by a plugin class extending MenuLinkDefault, you need to extend LocalTaskDefault.
    – 4uk4
    Commented Nov 26, 2018 at 19:19
  • I don't understand how LocalTaskDefault makes it possible to me to place dynamic local tasks depending on a entity value? I tried using a class extending LocalTaskDefault to declare a cache context but even this did not solve my problem. May I mention that I want second level tabs to be dynamic - those second level tabs are never shown except I clear cache and visit the page directly. Commented Nov 30, 2018 at 7:08
  • A cache context wouldn't help much, in this case you need to add the cache tag of the entity in getCacheTags() of the plugin. For the dynamic behavior, I think you can combine all three options, see the answer.
    – 4uk4
    Commented Nov 30, 2018 at 8:32

2 Answers 2

3

Derivatives are only rebuilt on a cache clear or when you call

\Drupal::service('plugin.manager.menu.local_task')->clearCachedDefinitions()

on changed configuration or content, but not on a regular request, this would be too slow.

Fully dynamical would be a LocalTaskDefault plugin, which you add to the yaml file:

mymodule.links.task.yml

mymodule.my_example_tab:
  route_name: default.static.route.name
  title: 'Default Static Title'
  base_route: example.base_route
  class: '\Drupal\mymodule\Plugin\Menu\MyLocalTask

The plugin class:

class MyLocalTask extends LocalTaskDefault {

  public function getRouteName() {
    return 'dynamic.route.name';
  }

  public function getTitle() {
    return 'Dynamic Title';
  }
}

The plugin doesn't need to have annotations, because it is discovered through the yaml file.

A local task can be assembled by information of all three methods at the same time, for example you could define the base route in a static yaml, the title in a derivative depending on a content entity and the route name in a plugin depending on dynamic conditions of the request.

2
  • +1 For your dynamic local tasks example. Even I doubt dynamic local tasks are the rightful approach for the OP's use case. Commented Mar 3, 2019 at 6:43
  • @MarioSteinitz, +1 for your answer bringing access into the picture. While you can in theory do all things you want in the custom plugin class, an access based solution is most times the better solution, especially for cache performance.
    – 4uk4
    Commented Mar 3, 2019 at 9:23
2

An alternative approach for your use case: Instead of using a deriver for the local tasks that is trying to evaluate the entity, just add all the local tasks statically. The evaluation of whether to show these tasks on your entity will then be delegated to the access check of their routes. If the current user doesn't have access to the routes, the local task won't be shown.

So your mymodule.links.task.yml may contain local tasks for all possible tabs including default, your candidacies, constituencies, committees and mandates.

For brevity, below just an example for the candidacies. You may extend it for all your remaining local tasks/routes:

entity.parliament_period.candidacies:
  route_name: entity.parliament_period.candidacies
  base_route: entity.parliament_period.canonical
  title: 'Candidacies'

When checking the access, you can use a custom controller or a dedicated EntityAccessControlHandler with according operations, if you defined your local tasks' links/forms as such within your entity annotation.

Here an extendable example with a custom controller. Define a custom access callback for your routes in mymodule.routing.yml:

entity.parliament_period.candidacies:
  path: '/my-path/{parliament_period}/candidacies'
  defaults:
    [...]
  options:
    parameters:
      parliament_period:
        type: 'entity:parliament_period'
  requirements:
    _custom_access: '\Drupal\mymodule\Controller\ParliamentPeriodController::accessCandidacies'

The access callback accessCandidacies receives the very same URL parameters as your route callback/form. Above options specify the {parliament_period} parameter as placeholder for your custom entity.

The callback checks the condition on the parliament period and returns an according AccessResult object. To cache it for your entity and in below example the current user only, you can add both as cache context dependency.

src/Controller/ParliamentPeriodController.php:

<?php

namespace Drupal\mymodule\Controller;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Controller\ControllerBase;
use Drupal\mymodule\Entity\ParliamentPeriodInterface;

/**
 * My parliament controller.
 */
class ParliamentPeriodController extends ControllerBase {

  /**
   * Check access to candidacies.
   *
   * @param \Drupal\mymodule\Entity\ParliamentPeriodInterface $parliament_period
   *   My parliament period.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   Access check result.
   */
  public function accessCandidacies(ParliamentPeriodInterface $parliament_period) {
    $account = $this->currentUser();

    return AccessResult::allowedIf(
      // Your original condition.
      $parliament_period->isElection()
      // Optional check for user permissions.
      && $account->hasPermission('view parliament_period')
    )
      ->addCacheableDependency($parliament_period)
      ->cachePerUser();
  }

}

(I made the assumption, that you defined an interface for your custom entity as well. You need to adapt the controller, if not.)

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.