Drupal 10 create local actions programmatically

Submitted by oioioooi on 03/08/2024 - 13:38

Local actions are UI components being menu shortcuts placed right under the local tasks tabs. These are actually menu items, but rendered as buttons.

Normally, local actions are defined in the YOUR_MODULE_NAME.links.action.yml file. To generate local actions dynamically it is required to implement a deriver class.

Derivative classes enable to register multiple plugin instances programmatically with minor changes. These inherit base logic from the deriving (base) plugin class, hence the name.

local-actions
The "+ Add content" button is a local action

Normally, one would click the "+ Add content" button, select the content type and be shown the node edit form. Let's improve the UI by providing a separate local action for each of the node types available.

In the example below, a separate local action (button) is generated for every node type that exists in the system.

Creating the menu local action entry

Edit (or create) a YOUR_MODULE_NAME.links.action.yml file. For the examples below, the sample is used as module name.

+sample.node.add:
+  class: Drupal\Core\Menu\LocalActionDefault
+  deriver: Drupal\sample\Plugin\Derivative\NodeAddCreateLocalAction

The sample.node.add key is arbitrary and can be anything, as long as it is meaningful. Under the class key define the class that we are deriving from - local actions, hence the name. Under the deriver key is the custom plugin implementation that takes care of generating the menu items (local actions).

Implementing the deriver class

Create a class under YOUR_MODULE_DIR/src/Plugin/Derivative/NodeAddCreateLocalAction.php

+<?php
+
+namespace Drupal\sample\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class NodeAddCreateLocalAction extends DeriverBase implements ContainerDeriverInterface
+{
+
+  /**
+   * {@inheritDoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id): static {
+    $instance = new static();
+
+    return $instance;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition): array {
+    return $this->derivatives;
+  }
+
+}

Retrieve node bundles and build the derivatives defitions.

namespace Drupal\sample\Plugin\Derivative;

 use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Symfony\Component\DependencyInjection\ContainerInterface;

 class NodeAddCreateLocalAction extends DeriverBase implements ContainerDeriverInterface
 {

+  use StringTranslationTrait;
+
+  /**
+   * Entity type bundle service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected EntityTypeBundleInfoInterface $entityTypeBundleInfo;
+
   /**
    * {@inheritDoc}
    */
   public static function create(ContainerInterface $container, $base_plugin_id): static {
     $instance = new static();

+    $instance->entityTypeBundleInfo = $container->get('entity_type.bundle.info');
+    $instance->setStringTranslation($container->get('string_translation'));
+
     return $instance;
   }

class NodeAddCreateLocalAction extends DeriverBase implements ContainerDeriverIn
    * {@inheritDoc}
    */
   public function getDerivativeDefinitions($base_plugin_definition): array {
+    $bundles = $this->entityTypeBundleInfo->getBundleInfo('node');
+
+    foreach ($bundles as $bundle => $bundle_info) {
+      $this->derivatives["sample.node.add.{$bundle}"] = [
+        'title' => $this->t('Create :label', [
+          ':label' => $bundle_info['label'],
+        ]),
+        'route_name' => 'node.add',
+        'route_parameters' => [
+          'node_type' => $bundle,
+        ],
+        'appears_on' => ['system.admin_content'],
+      ] + $base_plugin_definition;
+    }
+
     return $this->derivatives;
   }

New local action entries (derivatives) are assigned to $this->derivatives array. Each entry is an array of keys with values, similar how these would be defined in the *.links.action.yml file.

The appears_on key is assigned the route where the local actions would appear. Use the Devel module to introspect the route identifier for the page where new local actions should be placed. Lastly, assign leftover plugin defition settings in the $base_plugin_definition variable.

Check the result

Clear the Drupal caches and refresh the page where local actions are expected to appear.

local-actions-result
New local actions generated programmatically

Final thoughts

In a similar fashion, any menu type entry can be programmatically created. With the main difference being the base class that is derived from. The plugin deriving concept, in fact, goes far beyond the menu links and allows to create multiple instances of virtually any plugin.

Tags