How to define a local task in Drupal 8

By Ronald van Belzen | June 2, 2018

A local task in Drupal is a callback displayed as a tab. A local task must have a parent item in order for the tab to be rendered.

A well-known example that is part of Drupal core are the local taks for comments with the menu titles "Published comments" and "Unapproved comments". To demonstrate I am going to add an extra local task to those of comments. Let's say I made a new view that will display spam comments and in the advanced settings I have given that view the machine name "page_comment_spam".

The first step would be to create the routing to that view. In the routing the machine name of the view is given to the defaults _view parameter.

# mycomment.routing.yml

mycomment.admin_comment_spam:
  path: /admin/content/comment/spam
  defaults: 
    _title: 'Comment spam'
    _view: page_comment_spam
  requirements:
    _permission: 'administer comments'

This routing defines the callback I will need to define the local task. Instead of a view, I could also have define a controller function (with a defaults _controller parameter) or a form (with a defaults _form parameter), which is used more often. For this example it does not really make a difference.

The definition for the local task is as follows.

# mycomment.links.tasks.yml

mycomment.admin_comment_spam:
  title: 'Spam comments'
  route_name: mycomment.admin_comment_spam
  class: Drupal\mycomment\Plugin\Menu\LocalTask\SpamComments
  parent_id: comment.admin
  weight: 10

I hope that the fact that I named the routing name and the task name the same, does not confuse you in thinking that it needs to be the same name. It does not need to be.

Special in the above is that a class is being defined. This class definition is optional. I included it here to demonstrate how you can dynamically change the title of the tab with the help of a local task plugin.

What is important is that I defined the parent_id the same as the existing local tasks for comments, and that I defined the route_name to be used for the callback of the tab. I added some weight to the definition to make sure the tab is displayed to the right of the existing local tasks.

The plugin definition I adapted from the one being used in the (core) comments module. It adds a count to the tab link equal to the number of comments that will be displayed in the view.

/* /src/Plugin/Menu/LocalTask/SpamComments */

<?php

namespace Drupal\mycomment\Plugin\Menu\LocalTask;

use Drupal\comment\CommentStorageInterface;
use Drupal\Core\Menu\LocalTaskDefault;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Provides a local task that shows the amount of spam comments.
 */
class SpamComments extends LocalTaskDefault implements ContainerFactoryPluginInterface {
  use StringTranslationTrait;

  /**
   * The comment storage service.
   *
   * @var \Drupal\comment\CommentStorageInterface
   */
  protected $commentStorage;

  /**
   * Construct the UnapprovedComments object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param array $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\comment\CommentStorageInterface $comment_storage
   *   The comment storage service.
   */
  public function __construct(array $configuration, $plugin_id, array $plugin_definition, CommentStorageInterface $comment_storage) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->commentStorage = $comment_storage;
  }

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

  /**
   * {@inheritdoc}
   */
  public function getTitle(Request $request = NULL) {
    return $this->t('Spam comments (@count)', ['@count' => $this->getSpamCount()]);
  }
  
  /**
   * Returns the number of spam comments.
   * 
   * @return int
   *   The number of spam comments.
   */
  protected function getSpamCount() {
    $ids = $this->commentStorage->getQuery()
      ->exists('field_spam')
      ->condition('field_spam', TRUE)
      ->execute();
    return count($ids);
  }

}

In case you wondered, the field field_spam is a field I added to the existing comment fields. It is a boolean field that will not be present in the comments that existed at the moment that I added the field. The boolean indicates whether a comment is considered to be spam.

The main difference with the original plugin from comments, apart from the class name, is that I injected the comment storage into the class, because I needed it to be able to get the number of spam comments in the function getSpamCount().

Add new comment