[Idea] Add all items validation before execute

Created on 10 November 2023, about 1 year ago

Problem/Motivation

We have a action that mark payments how refunded or canceled, an user order can have more than one payment.

How a mistakes detection, we don't want execute the action if selected items affect to more that one user order.

This require review all selected items before to execute the complete batch.

Steps to reproduce

It's a new feature.

Proposed resolution

I implemented this feature in a custom module to a new operation.

I copy the code here to allow others to generalize it, or to future reference.

In the ".module" file I added a batch alter hook and a custom getList batch:

/**
 * Alter VBO cancel payment action.
 *
 * Implements hook_batch_alter().
 */
function mymodule_batch_alter(&$batch) {
  // Is this a WBO operation to cancel payment installments?
  if (isset($batch["sets"][0]["operations"][0][1][0]["action_id"]) && $batch["sets"][0]["operations"][0][1][0]["action_id"] === 'action_plugin_cancel_payment_installment') {
    // This bulk operation is starting? Populating list state.
    if (isset($batch["sets"][0]["operations"][0][0][0]) && $batch["sets"][0]["operations"][0][0][0] === 'Drupal\views_bulk_operations\ViewsBulkOperationsBatch') {
      if (isset($batch["sets"][0]["operations"][0][0][1])) {
        if ($batch["sets"][0]["operations"][0][0][1] === 'getList') {
          // The user select all items.
          $batch["sets"][0]["operations"][0][0] = '_preprocess_wbo_cancel_payment_installment';
        }
        elseif ($batch["sets"][0]["operations"][0][0][1] === 'operation') {
          // Does the user selected a few items?
          if ($batch["sets"][0]["operations"][0][1][0]["selected_count"] !== $batch["sets"][0]["operations"][0][1][0]["total_results"]) {
            $batch["sets"][0]["operations"][0][0] = '_preprocess_wbo_cancel_payment_installment';
          }
        }
        else {
          // Stop processing by a dangerous WBO module update.
          throw new Exception(t('The WBO module has changed its operations processing model.'));
        }
      }
    }
  }
}

/**
 * Preprocess WBO list to validate cancel process.
 *
 * It doesn't must allow process if it has different orders.
 * We can't stop a running batch from first item, always we execute the first
 * operations.
 *
 * @param array $data
 *   getList data parameter.
 * @param array $context
 *   $context data parameter.
 */
function _preprocess_wbo_cancel_payment_installment(array &$data, array &$context) {
  // Is this a selected all item operation?
  if ($data["selected_count"] === $data["total_results"]) {
    // Yes, execute original batch worker.
    $getListCallable = ['Drupal\views_bulk_operations\ViewsBulkOperationsBatch', 'getList'];
    call_user_func_array($getListCallable, [$data, $context]);
    $list = $context["results"]["list"];
  }
  else {
    // No, execute later.
    $list = $data["list"];
  }
  if (!isset($context["results"]["order_list"])) {
    // This list will remember each order affected.
    $context["results"]["order_list"] = [];
  }
  if (!isset($context["results"]["operations"])) {
    // If the user don't select all items and selected items will fail
    // this array entry don't exist and generate error.
    $context["results"]["operations"] = [];
  }
  // Capture each order affected.
  $entityTypeManager = \Drupal::entityTypeManager();
  $paymentInstallmentRepo = $entityTypeManager->getStorage('payment_installment');
  foreach ($list as $item) {
    $entity = $paymentInstallmentRepo->load($item[0]);
    if ($entity) {
      $context["results"]["order_list"][$entity->get('payment_order')->getString()] = 1;
    }
  }
  // Error detected?
  if (count($context["results"]["order_list"]) > 1) {
    // Force a clean stop.
    $context["finished"] = 1;
    $data['success'] = FALSE;
    // We are detected multiple orders in the cancel installment process.
    // This is so dangerous, we must stop it.
    // This will be detected in each operation executed.
    $context["results"]["security_stop"] = 1;
    // Display information to user.
    \Drupal::messenger()->addError(t('Multiple order detected!'));
    \Drupal::messenger()->addError(t('No payment installment was modified.'));
  }
  else {
    if ($data["selected_count"] !== $data["total_results"]) {
      // The user don't selected all items, after validation we need execute
      // the job.
      $getListCallable = ['Drupal\views_bulk_operations\ViewsBulkOperationsBatch', 'operation'];
      call_user_func_array($getListCallable, [$data, $context]);
    }
  }
}

And, in the custom operation in folder "web/modules/custom/mymodule/src/Plugin/Action/CancelPaymentInstallment.php":

namespace Drupal\mymodule\Plugin\Action;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\views_bulk_operations\Action\ViewsBulkOperationsActionBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides an Cancel Payment Installment Action.
 *
 * @todo Limit massive cancellation to not allow multiple orders at same time.
 * @see https://www.drupal.org/docs/8/modules/views-bulk-operations-vbo/advanced
 *
 * @Action(
 *   id = "action_plugin_cancel_payment_installment",
 *   label = @Translation("Cancel Installment"),
 *   type = "payment_installment"
 * )
 */
class CancelPaymentInstallment extends ViewsBulkOperationsActionBase implements ContainerFactoryPluginInterface {

  /**
   * The tempstore object.
   *
   * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
   */
  protected $tempStore;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * Constructs a new cancel_payment_installment object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin ID for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
   *   The tempstore factory.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   Current user.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
    $this->currentUser = $current_user;
    $this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm');
    $this->entityTypeManager = $entity_type_manager;

    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

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

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

  /**
   * {@inheritdoc}
   */
  public function execute($node = NULL) {
    if (isset($this->context["security_stop"])) {
      // The process has detected a security problem and need ignore any
      // future action.
      return;
    }
    // @todo Apply status change.
    $a = 1;
  }

}

Remaining tasks

Find a generic solution, maybe with a plugin model?

User interface changes

N/A

API changes

I don't known how, but I think that yes.

Data model changes

N/A

Feature request
Status

Active

Version

4.2

Component

Core

Created by

🇪🇸Spain psf_ Huelva

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Production build 0.71.5 2024