Custom actions in Drupal 8/9
Published by Alan Saunders on Sunday, February 28, 2021 - 18:00
Reading Time: 17 minutes
TitleWhat is an action?
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?
- Basic action
- Configurable action
- Custom action for views bulk operation module
Title[module name].info.yml
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
TitleBasic action
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();
}
}
TitleBasic action using views bulk operations
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();
}
}
TitleConfigurable action
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.
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.
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.
TitleConfigurable action using views bulk operations
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.
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.
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.
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.
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.
Title[module name].schema.yml
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.
Titlesystem.action.[action machine name].yml
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: { }
Related blog posts
Creating custom content entities in Drupal 8/9
Authored by Alan Saunders on Wednesday, March 24, 2021 - 22:00
Reading Time: 7 minutes
Queues in Drupal 8 and 9
Authored by Alan Saunders on Sunday, December 26, 2021 - 22:00
Reading Time: 5 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