Coding

Custom actions in Drupal 8/9

Published by Alan Saunders on Sunday, February 28, 2021 - 18:00

Reading Time: 17 minutes

Sections

Title
What is an action?

Body

An action is something that can be peformed against entities, these actions can be run on one or more entities at a time using Drupals Batch API. Common actions for entities such as nodes are un-publishing and publishing content.

What will we cover in this blog post?

  1. Basic action
  2. Configurable action
  3. Custom action for views bulk operation module

Title
[module name].info.yml

Body

For all types of actions that we will look at, we need to have a custom module skeleton setup, so we created a folder in the custom modules folder called custom_actions for example and we add a file called custom_actions.info.yml with the following code, as mentioned in previous blog posts, this will tell Drupal that we are adding a new module.

name: Custom actions
type: module
description: Provides some custom actions.
core_version_requirement: ^8 || ^9
package: Custom

Title
Basic action

Body

Now we can get on with creating our action(s), first we will look at creating a basic action.

First we need to ensure that you have inside your custom module, a folder called src with a folder for Plugin inside then another folder inside Plugin called Action and we will be adding out action classes into the Action folder.

Now create your class (in my case, the class file is called ConvertEnquiryToBooking.php), the code below is a simple action that I have created. The purpose of this simple action is to convert booking enquiries (that come in via a webform on a website) into confirmed booking entities (a custom entity). As this is a simple action, we extend the core ActionBase class and to take advantage of Dependency Injection, we implement the ContainerFactoryPluginInterface.

Above the opening class declaration, we need to add an annotation that informs Drupal that we are adding an action and we specify an id and a label that will appear in the action modules UI, the type parameter is the name of the entity that you are using, but can be left empty if this action should apply to all entity types.

In the execute function, you have access to the entity/ entities that you have selected in the view that the action has been enabled on, so in the execute function, you do what you want the action to do to the selected entities.

The access function ensures that the user wishing to execute this action is able to perform the required operation on the entity type.

<?php
namespace Drupal\custom_actions\Plugin\Action;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Entity\EntityStorageException;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Convert enquiry to booking.
 *
 * @Action(
 *   id = "convert_enquiry_to_booking",
 *   label = @Translation("Convert booking enquiry to booking"),
 *   type = "booking_entity"
 * )
 */
final class ConvertEnquiryToBooking extends ActionBase implements ContainerFactoryPluginInterface {

  /**
   * @var string
   */
    protected $entity_type = "booking_entity";

  /**
   * @var string
   */
    protected $date_format = 'Y-m-d\TH:i:s';

  /**
   * @var string
   */
    protected $mail_date_format = 'd/m/Y H:i:s';

  /**
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
    protected $configFactory;

  /**
   * @var \Drupal\Core\Session\AccountInterface
   */
    protected $account;

  /**
   * @var \Drupal\Core\Datetime\DateFormatter
   */
    protected $dateFormatter;

  /**
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
    protected $loggerFactory;

  /**
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
    protected $entityTypeManager;

  /**
   * @var \Drupal\Component\Datetime\TimeInterface
   */
    protected $timeInterface;

  /**
   * ConvertEnquiryToBooking constructor.
   * @param array $configuration
   * @param $plugin_id
   * @param $plugin_definition
   * @param ConfigFactoryInterface $config_factory
   * @param AccountInterface $account
   * @param DateFormatter $date_formatter
   * @param LoggerChannelFactoryInterface $logger_factory
   * @param EntityTypeManagerInterface $entity_type_manager
   * @param TimeInterface $time_interface
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    ConfigFactoryInterface $config_factory,
    AccountInterface $account,
    DateFormatter $date_formatter,
    LoggerChannelFactoryInterface $logger_factory,
    EntityTypeManagerInterface $entity_type_manager,
    TimeInterface $time_interface
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->configFactory = $config_factory;
    $this->account = $account;
    $this->dateFormatter = $date_formatter;
    $this->loggerFactory = $logger_factory->get('custom_actions');
    $this->entityTypeManager = $entity_type_manager;
    $this->timeInterface = $time_interface;
  }

  /**
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   * @param array $configuration
   * @param $plugin_id
   * @param $plugin_definition
   *
   * @return static
   */
    public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
      return new static(
        $configuration,
        $plugin_id,
        $plugin_definition,
        $container->get('config.factory'),
        $container->get('current_user'),
        $container->get('date.formatter'),
        $container->get('logger.factory'),
        $container->get('entity_type.manager'),
        $container->get('datetime.time')
      );
    }

  /**
     * {@inheritdoc}
     */
    public function execute($entity = NULL) {

        if ($entity->getSticky() == 1) {
            $this->messenger()->addError($this->t('You have already created a booking for this enquiry.'));
            return;
        }
        //Grab the uid from the current logged in user.
        $current_user_id = $this->account->id();
        //Grab the entities data.
        $data = $entity->getData();
        //Format the dates.
        $new_start_date = $this->dateFormatter->format(strtotime($data['booking_start_date']), 'custom', $this->date_format);
        $new_end_date = $this->dateFormatter->format(strtotime($data['booking_end_date']), 'custom', $this->date_format);

        //Setup an array and stored the values into the array.
        $values = [
            'id' => NULL,
            'type' => $this->entity_type,
            'name' => $data['booking_group_name'],
            'field_booking_date' => [
                'value' => $new_start_date,
                'end_value' => $new_end_date
            ],
            'field_booking_address' => [
                'country_code' => 'GB',
                'address_line1' => $data['booking_your_address']['address'],
                'address_line2' => empty($data['booking_your_address']['address2']) ?: $data['booking_your_address']['address2'],
                'locality' => $data['booking_your_address']['city'],
                'postal_code' => $data['booking_your_address']['postal_code'],
            ],
            'field_booking_anticipated_nos' => $data['booking_anticipated_numbers'],
            'field_booking_email' => $data['booking_your_email'],
            'field_booking_group_name' => $data['booking_group_name'],
            'field_booking_home_phone_number' => $data['booking_home_phone_number'],
            'field_booking_mobile_number' => $data['booking_your_mobile_number'],
            'field_booking_name' => $data['booking_your_name'],
            'field_booking_special_reqs' => $data['booking_special_requirements'],
            'field_booking_centre' => $data['booking_centre'],
            'field_booking_camping' => $data['booking_camping'],
            'field_number_of_tents' => $data['no_of_tents'],
            'field_booking_status' => 29,
            'uid' => $current_user_id,
            'revision_user' => $current_user_id,
            'status' => 1,
            'created' => $this->timeInterface->getRequestTime(),
        ];

        if ($entity->getSticky() == 0) {

            try {
                    //If no booking exists, create it.
                    $booking_storage = $this->entityTypeManager->getStorage($this->entity_type);
                    $booking = $booking_storage->create($values);
                    $booking->save();
                    $booking_msg = "Booking created: " . $data['booking_name'];
                    //Log an issue if something went wrong.
                    $this->loggerFactory->notice($this->t($booking_msg));
                    $entity->setSticky(TRUE)->save();
                    $webform_msg = "Booking enquiry marked as flagged, which means that the booking has been created.";
                    $this->loggerFactory->notice($this->t($webform_msg));

            } catch (EntityStorageException $e) {
                $message = "Could not save booking.";
                //Log an issue if something went wrong.
                $this->loggerFactory->error($this->t($message));
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
        $result = $object->access('update', $account, TRUE);
        return $return_as_object ? $result : $result->isAllowed();
    }
}

 

Title
Basic action using views bulk operations

Body

One downside to the basic action that we talked about above is that we don't have any way (easily anyhow) of giving the user a confirmation screen before the action is actually executed.

In Drupal 7, a module called Views Bulk Operations is available that gives you a number of different default actions out of the box that can be run against entities, so less need for custom code. There is also a Drupal 8/9 version available and the module will pick up custom actions too.

To make the action compatible with views bulk operations, I have tweaked the class that I made earlier slightly so that the views bulk operation module will be able to use the custom action, the views bulk operation may be able to use the action without needing to extend the views bulk operation classes, but this isn't something that I have fully tested, plus by not extended the views bulk operation classes, you won't be able to get the full benefits of the module. The differences between the two classes are;

  • Instead of extending the ActionBase class, we extend the ViewsBulkOperationsActionBase and then in the annotation we can set the confirm parameter to true and this will give us a confirmation screen before the action is actually run against the selected entities.

Below is the code that we have that uses the view bulk operations module functionality.

<?php
namespace Drupal\custom_actions\Plugin\Action;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Annotation\Action;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\views_bulk_operations\Action\ViewsBulkOperationsActionBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Convert enquiry to booking.
 *
 * @Action(
 *   id = "convert_enquiry_to_booking",
 *   label = @Translation("Convert booking enquiry to booking"),
 *   type = "booking_entity",
 *   confirm = TRUE
 * )
 */
final class ConvertEnquiryToBooking extends ViewsBulkOperationsActionBase implements ContainerFactoryPluginInterface {

  /**
   * @var string
   */
    protected $entity_type = "booking_entity";

  /**
   * @var string
   */
    protected $date_format = 'Y-m-d\TH:i:s';

  /**
   * @var string
   */
    protected $mail_date_format = 'd/m/Y H:i:s';

  /**
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
    protected $configFactory;

  /**
   * @var \Drupal\Core\Session\AccountInterface
   */
    protected $account;

  /**
   * @var \Drupal\Core\Datetime\DateFormatter
   */
    protected $dateFormatter;

  /**
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
    protected $loggerFactory;

  /**
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
    protected $entityTypeManager;

  /**
   * @var \Drupal\Component\Datetime\TimeInterface
   */
    protected $timeInterface;

  /**
   * ConvertEnquiryToBooking constructor.
   * @param array $configuration
   * @param $plugin_id
   * @param $plugin_definition
   * @param ConfigFactoryInterface $config_factory
   * @param AccountInterface $account
   * @param DateFormatter $date_formatter
   * @param LoggerChannelFactoryInterface $logger_factory
   * @param EntityTypeManagerInterface $entity_type_manager
   * @param TimeInterface $time_interface
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    ConfigFactoryInterface $config_factory,
    AccountInterface $account,
    DateFormatter $date_formatter,
    LoggerChannelFactoryInterface $logger_factory,
    EntityTypeManagerInterface $entity_type_manager,
    TimeInterface $time_interface
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->configFactory = $config_factory;
    $this->account = $account;
    $this->dateFormatter = $date_formatter;
    $this->loggerFactory = $logger_factory->get('custom_actions');
    $this->entityTypeManager = $entity_type_manager;
    $this->timeInterface = $time_interface;
  }

  /**
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   * @param array $configuration
   * @param $plugin_id
   * @param $plugin_definition
   *
   * @return static
   */
    public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
      return new static(
        $configuration,
        $plugin_id,
        $plugin_definition,
        $container->get('config.factory'),
        $container->get('current_user'),
        $container->get('date.formatter'),
        $container->get('logger.factory'),
        $container->get('entity_type.manager'),
        $container->get('datetime.time')
      );
    }

  /**
     * {@inheritdoc}
     */
    public function execute($entity = NULL) {

        if ($entity->getSticky() == 1) {
            $this->messenger()->addError($this->t('You have already created a booking for this enquiry.'));
            return;
        }
        //Grab the uid from the current logged in user.
        $current_user_id = $this->account->id();
        //Grab the entities data.
        $data = $entity->getData();
        //Format the dates.
        $new_start_date = $this->dateFormatter->format(strtotime($data['booking_start_date']), 'custom', $this->date_format);
        $new_end_date = $this->dateFormatter->format(strtotime($data['booking_end_date']), 'custom', $this->date_format);

        //Setup an array and stored the values into the array.
        $values = [
            'id' => NULL,
            'type' => $this->entity_type,
            'name' => $data['booking_group_name'],
            'field_booking_date' => [
                'value' => $new_start_date,
                'end_value' => $new_end_date
            ],
            'field_booking_address' => [
                'country_code' => 'GB',
                'address_line1' => $data['booking_your_address']['address'],
                'address_line2' => empty($data['booking_your_address']['address2']) ?: $data['booking_your_address']['address2'],
                'locality' => $data['booking_your_address']['city'],
                'postal_code' => $data['booking_your_address']['postal_code'],
            ],
            'field_booking_anticipated_nos' => $data['booking_anticipated_numbers'],
            'field_booking_email' => $data['booking_your_email'],
            'field_booking_group_name' => $data['booking_group_name'],
            'field_booking_home_phone_number' => $data['booking_home_phone_number'],
            'field_booking_mobile_number' => $data['booking_your_mobile_number'],
            'field_booking_name' => $data['booking_your_name'],
            'field_booking_special_reqs' => $data['booking_special_requirements'],
            'field_booking_centre' => $data['booking_centre'],
            'field_booking_camping' => $data['booking_camping'],
            'field_number_of_tents' => $data['no_of_tents'],
            'field_booking_status' => 29,
            'uid' => $current_user_id,
            'revision_user' => $current_user_id,
            'status' => 1,
            'created' => $this->timeInterface->getRequestTime(),
        ];

        if ($entity->getSticky() == 0) {

            try {
                    //If no booking exists, create it.
                    $booking_storage = $this->entityTypeManager->getStorage($this->entity_type);
                    $booking = $booking_storage->create($values);
                    $booking->save();
                    $booking_msg = "Booking created: " . $data['booking_name'];
                    //Log an issue if something went wrong.
                    $this->loggerFactory->notice($this->t($booking_msg));
                    $entity->setSticky(TRUE)->save();
                    $webform_msg = "Booking enquiry marked as flagged, which means that the booking has been created.";
                    $this->loggerFactory->notice($this->t($webform_msg));

            } catch (EntityStorageException $e) {
                $message = "Could not save booking.";
                //Log an issue if something went wrong.
                $this->loggerFactory->error($this->t($message));
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
        $result = $object->access('update', $account, TRUE);
        return $return_as_object ? $result : $result->isAllowed();
    }
}

 

Title
Configurable action

Body

Next we will look at a configurable action, this is where you can have one action that can do a number of different things, so you don't have to create a multiple different actions that contain 99.9% the same code.

In the below example, I have a custom booking entity that contains some bookings for a campsite, these bookings have a booking status field that allows me too set if the booking is; provisional, confirmed or cancelled. I could create 3 basic actions, but the code would be mostly the same except changing what status we want to change the bookings too.

The code is similar to a basic action, but instead extends cores ConfigurableActionBase class. We then add a configuration form that will allow us in the action UI to select which booking status that the booking should be set too. In my case, the booking statuses are terms in a taxonomy vocabulary, so using the Form API, I add a select list field that outputs the terms from the taxonomy vocabulary.

In the submit configuration form function we save the values into the actions configuration. Below is the code of our custom configurable action.

<?php
namespace Drupal\custom_actions\Plugin\Action;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Change Booking Status
 *
 * @Action(
 *   id = "change_booking_status",
 *   label = @Translation("Change booking status"),
 *   type = "booking_entity"
 * )
 */
final class ChangeBookingStatus extends ConfigurableActionBase implements ContainerFactoryPluginInterface {

  /**
   * @var string
   */
  protected $entity_type = "booking_entity";

  /**
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $account;

  /**
   * @var \Drupal\Core\Datetime\DateFormatter
   */
  protected $dateFormatter;

  /**
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $loggerFactory;

  /**
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected $timeInterface;

  /**
   * ConvertEnquiryToBooking constructor.
   * @param array $configuration
   * @param $plugin_id
   * @param $plugin_definition
   * @param ConfigFactoryInterface $config_factory
   * @param AccountInterface $account
   * @param DateFormatter $date_formatter
   * @param LoggerChannelFactoryInterface $logger_factory
   * @param EntityTypeManagerInterface $entity_type_manager
   * @param TimeInterface $time_interface
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    ConfigFactoryInterface $config_factory,
    AccountInterface $account,
    DateFormatter $date_formatter,
    LoggerChannelFactoryInterface $logger_factory,
    EntityTypeManagerInterface $entity_type_manager,
    TimeInterface $time_interface
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->configFactory = $config_factory;
    $this->account = $account;
    $this->dateFormatter = $date_formatter;
    $this->loggerFactory = $logger_factory->get('custom_actions');
    $this->entityTypeManager = $entity_type_manager;
    $this->timeInterface = $time_interface;
  }

  /**
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   * @param array $configuration
   * @param $plugin_id
   * @param $plugin_definition
   *
   * @return static
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('config.factory'),
      $container->get('current_user'),
      $container->get('date.formatter'),
      $container->get('logger.factory'),
      $container->get('entity_type.manager'),
      $container->get('datetime.time')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function execute($entity = NULL) {
    $entity->field_booking_status->target_id = $this->configuration['booking_status'];
    try {
      $entity->save();
      $this->messenger()->addStatus($this->t('Booking status has been updated.'));
    } catch(EntityStorageException $e) {
      $this->messenger()->addError($this->t('There was a problem updating the booking status, please try again later.'));
      $this->loggerFactory->error('%errormsg %errorstack', ['%errormsg' => $e->getMessage(), '%errorstack' => $e->getTraceAsString()]);
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function getDefaultConfiguration()
  {
    return [
      'booking_status' => '',
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
    $result = $object->access('update', $account, TRUE);
    return $return_as_object ? $result : $result->isAllowed();
  }

  /**
   * @inheritDoc
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state)
  {
    $vid = 'booking_statuses';
    try {
      $terms = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vid);
    } catch (InvalidPluginDefinitionException $e) {
    } catch (PluginNotFoundException $e) {
      $this->loggerFactory->error('%errormsg %errorstack', ['%errormsg' => $e->getMessage(), '%errorstack' => $e->getTraceAsString()]);
    }
    $options = [];

    foreach ($terms as $term) {
      try {
        $term_obj = $this->entityTypeManager->getStorage('taxonomy_term')->load($term->tid);
      } catch (InvalidPluginDefinitionException $e) {
      } catch (PluginNotFoundException $e) {
        $this->loggerFactory->error('%errormsg %errorstack', ['%errormsg' => $e->getMessage(), '%errorstack' => $e->getTraceAsString()]);
      }
      $options[$term_obj->id()] = $term_obj->label();
    }

    $form['booking_status'] = [
      '#type' => 'select',
      '#title' => t('Booking Status'),
      '#description' => t('The booking status that the booking should be changed to'),
      '#default_value' => $this->configuration['booking_status'],
      '#options' => $options,
      '#required' => TRUE,
    ];
    return $form;
  }

  /**
   * @inheritDoc
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state)
  {
    $this->configuration['booking_status'] = $form_state->getValue('booking_status');
  }
}

The below screenshot shows how the custom action created using the code above should show up in the Action UI. You can then add the action, so that permitted users can see and use the action.

Image
Image showing actions module config page
Body

You should then see a screen showing the custom form that we created earlier. In our case, we give the action a name and select the booking status that we want.

Now we repeat the process until we have all of the actions that we want.

Another downside to this is any new terms that get added to the taxonomy vocabulary, you would need to create the action in the action ui for it, so that permitted users will be able to see and use the action.

Image
Screenshot showing configure action page for change booking status
Image
Screenshot showing actions listing page once actions has been configured
Body

After you have added all of the actions you need, you should see them all listed in the action UI, as you can see from my screenshot above.

Now we need to update/ add a view, so that we can use our custom action. You need to add a field to the view for bulk update (if you are using cores action module). If you are using views bulk operation, then you'll need to select the views bulk operations option instead. You should then see a views configuration screen for the field that allows you to configure which actions should show up. The field should (to save issues/ confusion) be at the top of the list of fields.

Then after adding the field and saving the view, you should see the bulk update drop down at the top of the view. You can then select which entities that you want to affect, select the relevant action that you need and click the apply to selected entities button.

Image
Screenshot of adding the bulk update form to the view so that the actions can be used.
Image
Bulk update configuration form in the view.
Image
Screenshot showing the view with the options available.
Image
Screenshot showing booking once action had been executed.

Title
Configurable action using views bulk operations

Body

If we want to use Views Bulk Operations for our configurable action then all we need to do is extend ViewsBulkOperationsActionBase instead of ConfigurableActionBase and as you will see in the code below we are also setting the confirm parameter to true in the action annotation too, so that we can have a confirmation screen before the action is actually executed on the selected entities.

<?php
namespace Drupal\custom_actions\Plugin\Action;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Annotation\Action;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\views_bulk_operations\Action\ViewsBulkOperationsActionBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Change Booking Status
 *
 * @Action(
 *   id = "change_booking_status",
 *   label = @Translation("Change booking status"),
 *   type = "booking_entity",
 *   confirm = TRUE
 * )
 */
final class ChangeBookingStatus extends ViewsBulkOperationsActionBase implements ContainerFactoryPluginInterface {

  use StringTranslationTrait;

  /**
   * @var string
   */
  protected $entity_type = "booking_entity";

  /**
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $account;

  /**
   * @var \Drupal\Core\Datetime\DateFormatter
   */
  protected $dateFormatter;

  /**
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $loggerFactory;

  /**
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected $timeInterface;

  /**
   * ConvertEnquiryToBooking constructor.
   * @param array $configuration
   * @param $plugin_id
   * @param $plugin_definition
   * @param ConfigFactoryInterface $config_factory
   * @param AccountInterface $account
   * @param DateFormatter $date_formatter
   * @param LoggerChannelFactoryInterface $logger_factory
   * @param EntityTypeManagerInterface $entity_type_manager
   * @param TimeInterface $time_interface
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    ConfigFactoryInterface $config_factory,
    AccountInterface $account,
    DateFormatter $date_formatter,
    LoggerChannelFactoryInterface $logger_factory,
    EntityTypeManagerInterface $entity_type_manager,
    TimeInterface $time_interface
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->configFactory = $config_factory;
    $this->account = $account;
    $this->dateFormatter = $date_formatter;
    $this->loggerFactory = $logger_factory->get('custom_actions');
    $this->entityTypeManager = $entity_type_manager;
    $this->timeInterface = $time_interface;
  }

  /**
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   * @param array $configuration
   * @param $plugin_id
   * @param $plugin_definition
   *
   * @return static
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('config.factory'),
      $container->get('current_user'),
      $container->get('date.formatter'),
      $container->get('logger.factory'),
      $container->get('entity_type.manager'),
      $container->get('datetime.time')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function execute($entity = NULL) {
    $entity->field_booking_status->target_id = $this->configuration['booking_status'];
    try {
      $entity->save();
      $this->messenger()->addStatus($this->t('Booking status has been updated.'));
    } catch(EntityStorageException $e) {
      $this->messenger()->addError($this->t('There was a problem updating the booking status, please try again later.'));
      $this->loggerFactory->error('%errormsg %errorstack', ['%errormsg' => $e->getMessage(), '%errorstack' => $e->getTraceAsString()]);
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function getDefaultConfiguration()
  {
    return [
      'booking_status' => '',
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
    $result = $object->access('update', $account, TRUE);
    return $return_as_object ? $result : $result->isAllowed();
  }

  /**
   * @inheritDoc
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state)
  {
    $vid = 'booking_statuses';
    $terms = "";
    try {
      $terms = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vid);
    } catch (InvalidPluginDefinitionException $e) {
    } catch (PluginNotFoundException $e) {
      $this->loggerFactory->error('%errormsg %errorstack', ['%errormsg' => $e->getMessage(), '%errorstack' => $e->getTraceAsString()]);
    }
    $options = [];

    foreach ($terms as $term) {
      $term_obj = "";
      try {
        $term_obj = $this->entityTypeManager->getStorage('taxonomy_term')->load($term->tid);
      } catch (InvalidPluginDefinitionException $e) {
      } catch (PluginNotFoundException $e) {
        $this->loggerFactory->error('%errormsg %errorstack', ['%errormsg' => $e->getMessage(), '%errorstack' => $e->getTraceAsString()]);
      }
      $options[$term_obj->id()] = $term_obj->label();
    }

    $form['booking_status'] = [
      '#type' => 'select',
      '#title' => $this->t('Booking Status'),
      '#description' => $this->t('The booking status that the booking should be changed to'),
      '#default_value' => $this->configuration['booking_status'],
      '#options' => $options,
      '#required' => TRUE,
    ];
    return $form;
  }

  /**
   * @inheritDoc
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state)
  {
    $this->configuration['booking_status'] = $form_state->getValue('booking_status');
  }
}

The following screenshots will show me configuring the views bulk operation field mentioned earlier, which is similar to the bulk update that I talked about earlier. Once we add the field to the view, we can configure which actions we want to show. You should then (once saved) see the selected actions showing up in your view.

Image
Showing views bulk operation options in view edit screen
Image
Views bulk operation custom action showing in view front end
Body

Then it is a similar process of selecting which items that you want to update, selecting the action you want to use then clicking the apply to selected entities button.

Now views bulk operations does provide an action for modifying entity values, which allows permitted users to update any field on the selected entities. A downside to this is that this could be too confusing for some users to remember what they need to do, but it does depend on your project and user demographic.

I opted for creating custom actions, not only for experience in creating custom actions, but also for simplification for end users.

Image
Image showing selected booking and selected action of change booking status
Body

Then we should see a screen that shows our custom form that we added to our action that allows you the permitted user to select in our case what the booking status that the selected entities should be set too.

The below screenshot show the screen with our custom form.

Image
Image showing screen where the booking status can be changed.
Body

With a normal action, you don't get a confirmation screen, but as shown in the screenshot below, we can easily enable a confirmation screen for the custom actions using the views bulk operation module. This helps prevent entities being mistakenly updated or maybe even deleted depending on the action you are wanted to trigger.

Image
Screen allowing us to confirm that we want to execute the action
Body

Then once the action has been ran, you get a visual message to inform you that the action has been run against the selected entities successfully.

Image
Screenshot showing the booking after the action was ran

Title
[module name].schema.yml

Body

In the module's config folder, we provide the schema (config/schema/custom_action.schema.yml) of the configuration of the action that we have implemented.

Create a config/schema/custom_action.schema.yml file:

action.configuration.convert_enquiry_to_booking:
  type: action_configuration_default
  label: 'Convert booking enquiry to booking'
action.configuration.change_booking_status:
  type: action_configuration_default
  label: 'Change booking status'

The contents of the files will look similar to the above, the key is in the format of action.configuration.action_plugin_id, we specify the type and set a label for each action.

Title
system.action.[action machine name].yml

Body

We now add the config for the actions that we set in our schema yml file mentioned above, this is so that when we install the module the configuration of the actions can be imported, so that our actions will be added to the site and depending on the type of action that you have created, will dictate whether the action is ready to be added to a view and used or if further configuration is needed.

Now we can either add the configuration into folders config/install or config/optional inside the module, the difference between the two is:

  • install: The folder can contain configuration yml files. All configuration will be installed, if any config fails module can't be installed.
  • optional: The folder can contain configuration yml files. All configuration will be installed if possible. If config has missing dependencies, it won't be installed.

Config installation only happens when the module itself is installed. For optional config it can install missing configuration when dependencies are met by a new module being installed.

Before is the configuration file that we have added for one of the actions that we have added through this blog post. The file names of this configuration is in the format of system.action.action_machine_name.yml, this is so that Drupal can recognise what the configuration file is for and should be able to import the configuration relatively painlessly, providing there are no issues in the yml file makeup.

# File system.action.change_booking_status.yml
langcode: en
status: true
dependencies:
  module:
    - booking_entity
id: change_booking_status
label: 'Change booking status'
type: booking_entity
plugin: change_booking_status
configuration: {  }

 

Categories:

Related blog posts

Creating custom tokens in Drupal 8/9

Authored by Alan Saunders on Sunday, January 17, 2021 - 10:00
Reading Time: 8 minutes

Events and Event Subscribers in Drupal 8 and 9

Authored by Alan Saunders on Wednesday, June 30, 2021 - 10:00
Reading Time: 9 minutes

Queues in Drupal 8 and 9

Authored by Alan Saunders on Sunday, December 26, 2021 - 22:00
Reading Time: 5 minutes