Do you want to know how to Drupal?

Let's Drupal

Drupal 9

How to add a local task to custom page in Drupal?

In the previous post How to add a local task (tab) in Drupal?, we have reviewed how to add a static tab to the page with tabs, but what if we want to add a tab to our custom page?

Lets imagine we have a parent page and two child pages

E-shop Settings

  • Products settings
  • Categories settings

You can find the final result here: https://github.com/LOBsTerr/drupal-modules-examples/tree/master/local_tasks

First we will add custom pages and routes for them, for this we need to add to your controller (in my case LocalTasksController.php)

public function eshopSettings() {
    return [
      '#markup' => $this->t('This is a parent settings page. This page has two child pages: <a href="@products_settings_url">Products settings</a> and <a href="@categories_settings_url">Categories settings</a>', [
        '@products_settings_url' => Url::fromRoute('local_tasks.products_settings')->toString(),
        '@categories_settings_url' => Url::fromRoute('local_tasks.categories_settings')->toString(),
      ]),
    ];
  }

  public function eshopMetaSettings() {
    return [
      '#markup' => $this->t('This is a meta settings page'),
    ];
  }

  public function productsSettings() {
    return [
      '#markup' => $this->t('This is a products settings page'),
    ];
  }

  public function categoriesSettings() {
    return [
      '#markup' => $this->t('This is a categories settings page'),
    ];
  }

Then we will provide routes for them

# Routes for custom page (eshop) tabs.
local_tasks.eshop_settings:
  path: '/eshop-settings'
  defaults:
    _controller: '\Drupal\local_tasks\Controller\LocalTasksController::eshopSettings'
    _title: 'E-shop settings'
  requirements:
    _permission: 'access content'

local_tasks.eshop_meta_settings:
  path: '/eshop-meta-settings'
  defaults:
    _controller: '\Drupal\local_tasks\Controller\LocalTasksController::eshopMetaSettings'
    _title: 'E-shop meta settings'
  requirements:
    _permission: 'access content'

local_tasks.products_settings:
  path: '/eshop-settings/products-settings'
  defaults:
    _controller: '\Drupal\local_tasks\Controller\LocalTasksController::productsSettings'
    _title: 'Products settings'
  requirements:
    _permission: 'access content'

local_tasks.categories_settings:
  path: '/eshop-settings/categories-settings'
  defaults:
    _controller: '\Drupal\local_tasks\Controller\LocalTasksController::categoriesSettings'
    _title: 'Categories settings'
  requirements:
    _permission: 'access content'

Once it is done, you can check these pages.

/eshop-settings

/eshop-meta-settings

/eshop-settings/products-settings

/eshop-settings/categories-settings

The next step to add tabs. First we define the default tab. For the default tab the base_route should be equal route_name 

# in this example we add tabs for custom page.
local_tasks.eshop_settings: # The ID of local task is the same as route, to make it easier to control.
  route_name: 'local_tasks.eshop_settings' # A route for e-shop settings, provided in local_tasks.routing.yml.
  title: 'Eshop settings' # This string will be displayed as a title of a tab.
  base_route: 'local_tasks.eshop_settings' # !!!important For default tab the base_route should be the same as route_name.
  weight: 10

local_tasks.eshop_meta_settings: # The ID of local task is the same as route, to make it easier to control.
  route_name: 'local_tasks.eshop_meta_settings' # A route for e-shop settings, provided in local_tasks.routing.yml.
  title: 'Eshop meta settings' # This string will be displayed as a title of a tab.
  base_route: 'local_tasks.eshop_settings' # This will allow to display it in the same level with "E-shop settings"
  weight: 20

local_tasks.products_settings: # The ID of local task is the same as route, to make it easier to control.
  route_name: 'local_tasks.products_settings' # A route for e-shop settings, provided in local_tasks.routing.yml.
  title: 'Products settings' # This string will be displayed as a title of a tab.
  base_route: 'local_tasks.eshop_settings' # Because we want to group tabs we should set to default one, in our case E-shop settings.
  parent_id: 'local_tasks.eshop_settings' # This will display item under "Eshop settings" tab.
  weight: 10

local_tasks.categories_settings: # The ID of local task is the same as route, to make it easier to control.
  route_name: 'local_tasks.categories_settings' # A route for e-shop settings, provided in local_tasks.routing.yml.
  title: 'Categories settings' # This string will be displayed as a title of a tab.
  base_route: 'local_tasks.eshop_settings' # Because we want to group tabs we should set to default one, in our case E-shop settings.
  parent_id: 'local_tasks.eshop_settings' # This will display item under "Eshop settings" tab.
  weight: 20

Clean the cache and open /eshop-settings, you will see your tabs. At that moment all tabs are in the same level, but we want them displayed as a sublevel. For these we need to define parent_id and set it to local_tasks.eshop_settings in order to display them as under "E-shop settings".

Keep in mind that there should be at least two tabs in the level. In other case the tabs will be hidden.

 

 


How to add a local task (tab) in Drupal?

In Drupal we can display secondary menu as tabs. If you don't know what it is open any content item (node) as administrator and on the top you can see:

  • View 
  • Edit
  • Delete

and so on

In this post we will review how to add a static local task

In order to have a new tab you need to create a new YML file in the root of your module. This file has a specific name your_module_name.links.task.yml. You have to replace your_module_name with the name of your module

Then you can define the local task you want to provide in the next format  

your_module.admin: # Unique id of your tasks normally prefixed with route name or entity name 
  route_name: your_module.custom_settings # Route which will be opened, when we click on the tab
  title: 'Settings' #Title of your tab
  base_route: entity.node.canonical #Parent route (the default tab) 
  parent_id: entity.node.canonical # Optional: We define the parent tab id here
  weight: 100 # Defines the order for the tab

Let's review a simple example. I have module local_tasks. You can find it here  https://github.com/LOBsTerr/drupal-modules-examples/tree/master/local_tasks or create your own.

I have a custom page, which will provide the extra settings for nodes, in fact it will just display the title of a node for a sake of simplicity. This a route for which we are going to add a tab. You need to add it to local_tasks.routing.yml

 

local_tasks.node_extra_settings:
  path: '/node/{node}/extra-settings'
  defaults:
    _controller: '\Drupal\local_tasks\Controller\LocalTasksController::nodeExtraSettings'
    _title: 'Node extra settings'
  requirements:
    _permission: 'access content'
  options:
    parameters:
      node:
        type: 'entity:node'

 

The definition of the tab you need to add to local_tasks.links.task.yml

# In this example we add an additional tab to the node page
local_tasks.node_extra_settings: # The ID of local task is the same as route, to make it easier to control.
  route_name: 'local_tasks.node_extra_settings' # The name of our route, provided in local_tasks.routing.yml.
  title: 'Extra settings' # This string will be displayed as a title of a tab.
  base_route: 'entity.node.canonical' # We use node entity in order to group tabs together.
  weight: 100

Now, you need install the module local_tasks or to clean the cache if you do it your own module.

Open any node (node/[NID]), you should see a new tab "Extra settings". Also, you can open directly the page node/[NID]/extra-settings

In the next posts, we will review how to add sublevels of tabs, to define the default local task and how to add local tasks dynamically


How to alter options for dropdown (select input) in Drupal

Sometimes we need to alter or restrict some options for dropdown of entity reference field. One of the solution to use views and provide views entity reference widget, but in some cases the logic would be too complex and we will have to implement custom plugins for views. The second solution could be just replace options for dropdown widget. For this we need implement "hook_field_widget_form_alter"

/**
 * Implements hook_field_widget_form_alter().
 */
function your_module_field_widget_form_alter(
   if (!empty($field_definition) && $field_definition->getName() == 'fut_collection') {
    $element['#options'] = _your_module_get_input_options();
  }
}

/**
 * Gets group collection options.
 */
function _your_module_get_input_options() {
   $options = [];
   // here your custom logic to get ids.
   $items = Term::loadMultiple($ids);
   if ($items ) {
      // a default empty value.
      $options['_none'] = t('- None -');
      foreach ($items as $item) {
        $options[$item->id()] = $item->getName();
      }
   }

   return $options;
}

Keep in mind that you have to use the entity ids of entity type for your entity reference field. Also, if you want to provide the default value without any value, you should use this code:

$options['_none'] = t('- None -');

and not like

$options[''] = t('- None -');

In other case you can face some unexpected bugs


How to add a SQL index to existing entity tables

Drupal 8 provides a nice Entity API, which handles a lot of complicated mechanisms for us automatically. For example, the handling of tables for Content Entity. In some cases if we are fetching frequently entity tables and we need to improve the performance by providing additional indexes in database tables. There are different ways to do so. The first option you can think of to add directly using SQL query or Schema API. Obviously, it is not a the best way, because a lot of thing can go wrong here.

Luckily Drupal 8 provides nice way to do it.

First of all we need to define a Storage Schema class. Let's take as an example node module:

<?php

namespace Drupal\node;

use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Core\Field\FieldStorageDefinitionInterface;

/**
 * Defines the node schema handler.
 */
class NodeStorageSchema extends SqlContentEntityStorageSchema {

  /**
   * {@inheritdoc}
   */
  protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
    $schema = parent::getEntitySchema($entity_type, $reset);

    if ($data_table = $this->storage->getDataTable()) {
      $schema[$data_table]['indexes'] += [
        'node__frontpage' => ['promote', 'status', 'sticky', 'created'],
        'node__title_type' => ['title', ['type', 4]],
      ];
    }

    return $schema;
  }

  /**
   * {@inheritdoc}
   */
  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
    $schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
    $field_name = $storage_definition->getName();

    if ($table_name == 'node_revision') {
      switch ($field_name) {
        case 'langcode':
          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
          break;

        case 'revision_uid':
          $this->addSharedTableFieldForeignKey($storage_definition, $schema, 'users', 'uid');
          break;
      }
    }

    if ($table_name == 'node_field_data') {
      switch ($field_name) {
        case 'promote':
        case 'status':
        case 'sticky':
        case 'title':
          // Improves the performance of the indexes defined
          // in getEntitySchema().
          $schema['fields'][$field_name]['not null'] = TRUE;
          break;

        case 'changed':
        case 'created':
          // @todo Revisit index definitions:
          //   https://www.drupal.org/node/2015277.
          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
          break;
      }
    }

    return $schema;
  }

}


I will explain a little bit the code above before we continue. We have few options here

1) Add composite index, which includes several fields like this:
 

 if ($data_table = $this->storage->getDataTable()) {
      $schema[$data_table]['indexes'] += [
        'node__frontpage' => ['promote', 'status', 'sticky', 'created'],
        'node__title_type' => ['title', ['type', 4]],
      ];
}

2) Add an index for specific field:

case 'langcode':
          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
          break;

3) Add a foreign key

case 'revision_uid':
          $this->addSharedTableFieldForeignKey($storage_definition, $schema, 'users', 'uid');
          break;

4) Add additional properties for the fields

case 'title':
          // Improves the performance of the indexes defined
          // in getEntitySchema().
          $schema['fields'][$field_name]['not null'] = TRUE;
          break;

 

Now, when we have a class for Storage Schema, we need to add it to entity definition. Check Drupal\node\Entity\Node.php

 *   handlers = {
 *     "storage" = "Drupal\node\NodeStorage",
 *     "storage_schema" = "Drupal\node\NodeStorageSchema",
 *     "view_builder" = "Drupal\node\NodeViewBuilder",

No we to update our entity and set Storage Schema class for existing entity. We can do it using hook update

<?php
use Drupal\your_module\YourStorageSchema;

/**
 * Add storage schema to entity type.
 */
function your_module_update_8017() {
  $manager = \Drupal::entityDefinitionUpdateManager();

  // Get the current entity type definition, ensure the storage schema
  // class is set.
  $entity_type = $manager->getEntityType('enity_type')
    ->setHandlerClass('storage_schema', YourStorageSchema::class);

  // Regenerate entity type indexes.
  $manager->updateEntityType($entity_type);
}

Or if we have already had the Storage Schema class before, we can just update Entity type

<?php
use Drupal\your_module\YourStorageSchema;

/**
 * Update entity type.
 */
function your_module_update_8017() {
  $manager = \Drupal::entityDefinitionUpdateManager();
  $entity_type = $manager->getEntityType('enity_type');

  // Regenerate entity type indexes.
  $manager->updateEntityType($entity_type);
}

 


How to delete entity in Drupal?

In order to delete an entity in Drupal we can use entity storage to load an entity and then call "delete" method for it.

 

// Delete a node.
$node = \Drupal::entityTypeManager()->getStorage('node')->load(1);
if (!empty($node)) {
   $node->delete();
}

 

Or we can load an entity using static method "load" and then remove it.

 

// Delete a node.
$node = Node::load(1);
if (!empty($node)) {
   $node->delete();
}

 

Delete multiple nodes in one operation.

 

\Drupal::entityTypeManager()->getStorage('node')->delete([
   $nid1 => $node1,
   $nid2 => $node2,
]);

 


How to edit an Entity in Drupal?

In order to edit an entity in Drupal firstly we need to load it. We can use the static method of specific entity class, in the current example it is Node class

 

$node = Node::load(1);

 

We can also use entity storage to load entities

 

// Load single entity
$entity = \Drupal::entityTypeManager()->getStorage('node')->load(1);

// Load multiple entities
$entities = \Drupal::entityTypeManager()->getStorage($entity_type)->loadMultiple([1, 2, 3]);

 

Then you can set necessary fields and properties of the entity

 

// Call the specific setter
$node->setTitle('new Title');

// Call general set method
$node->set('body', 'Body text');

// Save the entity
$node->save();

 


How to create an Entity in Drupal?

In order to create an entity in Drupal 8 in a general case we need to Entity type manager to get a storage for a specific entity type, for example node 

// Use the entity manager to get node storage.
$node_storage = \Drupal::entityTypeManager()->getStorage('node');

// Call create method passing values for a new node entity.
$node_storage->create([
   'type' => 'page', 
   'title' => 'Page title',
]);

Or we can write it shorter

$node = \Drupal::entityTypeManager()->getStorage('node')->create(['type' => 'page', 'title' => 'Page title']);

We can also call a wrapper - a function, which provides the same functionality. I don't recommend to use it, because it is deprecated and  can be removed at any moment

$node = entity_create('node', [
  'type' => 'page',
  'title' => 'Page title',
  'body' => 'This a text of your page.',
]);

We can also call directly call method create of a specific entity

$node = Node::create([
  'type' => 'page',
  'title' => 'The page title',
]);