Here is my use case:

1-I use VBO to select some nodes in a view
2-I run a custom action on those nodes
3-In function executeMultiple of my custom action, I populate an array; something like

private array_that_should_be_used_in_the_redirection;

public function executeMultiple(array $objects) {
    $results = [];
    foreach ($objects as $entity) {
      $results[] = $this->execute($entity);
      $this->array_that_should_be_used_in_the_redirection[]=...some logic; 
    }
    return $results;
  }

Thus my question is: how to redirect to a form after a VBO run.
Knowing that this form must use $this->array_that_should_be_used_in_the_redirection as an argument.

Where I am:
In function executeProcessing (class ViewsBulkOperationsActionProcessor) I found that the finished function is called at the end of the process:

ViewsBulkOperationsBatch::finished(TRUE, $results, []);

But I don't know how/where to override/subclass the ViewsBulkOperationsActionProcessor in order to redirect to a form
(Or maybe there is a better way to achive this)

Comments

DuneBL created an issue. See original summary.

  • Graber committed 30b07e2 on 8.x-3.x
    Issue #3042535: How to redirect after a VBO run and use an array...
graber’s picture

Status: Active » Needs review

Hi, just made it possible with the above commit.

You do it like this from you action plugin:

  /**
   * {@inheritdoc}
   */
  public function setContext(array &$context) {
    $this->context['sandbox'] = &$context['sandbox'];
    $this->context['results'] = &$context['results'];
    foreach ($context as $key => $item) {
      if ($key === 'sandbox' || $key === 'results') {
        continue;
      }
      $this->context[$key] = $item;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function execute($entity = NULL) {
    // Execute..
    // When you know this is the last operation:
    $this->context['results']['redirect_url'] = \Drupal\Core\Url::fromRoute('system.admin');

graber’s picture

of course add a the Drupal\Core\Url namespace and conform to standards etc ;)

graber’s picture

also this works only if batching is enabled so would be good to have that for non-batch operations as well but that needs a change in a few places. For later if someone will need this.

johnpitcairn’s picture

I could use a non-batched version.

I don't really want to do much processing in the action apart from assembling a set of URL parameters based on the selected entities, then redirect to a custom controller route that will do most of my processing. I guess I can set my batch size high, but non-batched would be good.

johnpitcairn’s picture

Actually it's likely we will need to batch the processing somewhere anyway, so we might as well do our processing in the action and stash the result in private tempstore for the redirect page controller to pick up. Thanks, this is working well.

dunebl’s picture

@graber This is so nice you have done this!

Same as John Pitcairn, I had to use the private tempstore to send the results to my redirected form

Here is how I have implemented this feature (for anybody who would like to do the same)... :

1-Add the user.private_tempstore service as dependency injection in my plugin action [not mandatory]

    private $_tempstore;

    public function __construct(array $configuration, $plugin_id, $plugin_definition, PrivateTempStoreFactory $temp_store_factory)
    {
        parent::__construct($configuration, $plugin_id, $plugin_definition);
        $this->_tempstore = $temp_store_factory->get('my_module');
    }
    public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition)
    {
        return new static($configuration, $plugin_id, $plugin_definition, 
            $container->get('user.private_tempstore'));
    }
=>Don't forget to <code>implements ContainerFactoryPluginInterface

2-Adapt the setContext funtion in my plugin action:

    public function setContext(array &$context)
    {
        $this->context['sandbox'] = &$context['sandbox'];
        $this->context['results'] = &$context['results'];
        foreach ($context as $key => $item) {
            if ($key === 'sandbox' || $key === 'redirect_url') {
                continue;
            }
            $this->context[$key] = $item;
        }
    }

Question: looking at this code, I have the feeling that it could be better to inherit ViewsBulkOperationsActionBase instead of ActionBase... and if yes, maybe this code could be included in ViewsBulkOperationsActionBase

3-Set the redirect url and store the array within executeMultiple

    public function executeMultiple(array $soupes)
    {
        $this->array_that_should_be_used_in_the_redirection = [];
        parent::executeMultiple($soupes); //$this->array_that_should_be_used_in_the_redirection is populated
        $this->_tempstore->set('my_array', $this->array_that_should_be_used_in_the_redirection);
        $this->context['results']['redirect_url'] = \Drupal\Core\Url::fromRoute('route_to_my_form');
    }

4-And here is the code to create the form (with dependency injection)

   private $my_array_constructed_in_vbo;
    public function __construct(PrivateTempStoreFactory $temp_store_factory)
    {
        $this->my_array_constructed_in_vbo = $temp_store_factory->get('my_module')->get('my_array');
    }
    public static function create(ContainerInterface $container)
    {
        return new static(
            $container->get('user.private_tempstore')
            );
    }
    public function buildForm(array $form, FormStateInterface $form_state)
    {
        foreach ($this->_ingredients as $key=>$value) {
            dpm($value);
        }
        return $form;
    }

5-route definition:

route_to_my_form:
  path: '/myform'
  defaults:
    _form: '\Drupal\my_module\Form\MyForm'
    _title: 'Form after VBO'
  requirements:
    _permission: 'administer site configuration'

I hope, that this could help someone

johnpitcairn’s picture

I'm extending ViewsBulkOperationsActionBase.

Note user.private_tempstore is deprecated - you should use tempstore.private, which is fully backwards-compatible.

dunebl’s picture

@John Pitcairn: thank you for the hint!

@Graber Maybe I don't understand the goal, but why should we write the setContext method (if extending ViewsBulkOperationsActionBase). I mean why not updating the setContext method in ViewsBulkOperationsActionBase with this line $this->context['results'] = &$context['results'];?

graber’s picture

@DuneBL the same reason why we have interfaces and method and property visibility declarations. If we don't have to give access to something - we don't. At least that's the way I see it, this makes everything more controllable and reduces possible vulnerabilities. I guess some older dev could explain it better ;)

dunebl’s picture

@Graber ok, I think it goes over my head, but adding this line will not provide any access to the user a of the class. It will only simplify the way he will have to comply with the url redirection... Don't take your time to answer me, as this is not an educated comment. And thank you again!

svendecabooter’s picture

My use case might be a bit different, but here is how I solved a redirect from VBO, that does something with the selected entities:

My custom action plugin:


namespace Drupal\my_module\Plugin\Action;

use Drupal\views_bulk_operations\Action\ViewsBulkOperationsActionBase;

/**
 * Redirects to a delivery note creation form.
 *
 * @Action(
 *   id = "delivery_note_create",
 *   label = @Translation("Create delivery note"),
 *   type = "package",
 *   confirm_form_route_name = "entity.delivery_note.add_form",
 *   pass_view = TRUE
 * )
 */
class CreateDeliveryNote extends ViewsBulkOperationsActionBase {

  /**
   * {@inheritdoc}
   */
  public function executeMultiple(array $entities) {
    // This action doesn't actually do anything.
    // The selected entity IDs get passed to
    // \Drupal\my_module\Form\DeliveryNoteAddForm instead.
  }

  ...
}

Key element here is the "confirm_form_route_name" property in the annotation, which is the route where we want to redirect to, after having selected entities via VBO.

Then I have a custom form, which in my case is a content entity form where I want to pre-select the entities that were selected via VBO:


namespace Drupal\my_module\Form;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Url;
use Drupal\views_bulk_operations\Form\ViewsBulkOperationsFormTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Form controller for Delivery note add forms.
 */
class DeliveryNoteAddForm extends ContentEntityForm {

  use ViewsBulkOperationsFormTrait;

  /**
   * The tempstore factory.
   *
   * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
   */
  protected $tempStoreFactory;

  /**
   * Constructs a ContentEntityForm object.
   *
   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
   *   The entity manager.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
   *   The entity type bundle service.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
   *   The tempstore factory.
   */
  public function __construct(EntityManagerInterface $entity_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
    parent::__construct($entity_manager, $entity_type_bundle_info, $time);
    $this->tempStoreFactory = $temp_store_factory;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity.manager'),
      $container->get('entity_type.bundle.info'),
      $container->get('datetime.time'),
      $container->get('tempstore.private')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $view_data = $this->getTempstoreData('[VIEW_NAME]', '[DISPLAY_ID]');

    // Get all entities selected through VBO.
    $selected_ids = [];
    if ($view_data['list']) {
      foreach ($view_data['list'] as $selected_data) {
        [, $langcode, $entity_type_id, $id] = $selected_data;
        selected_ids[] = $id;
      }
    }

    (... custom logic here to do something with the selected entity IDs)

    $form = parent::buildForm($form, $form_state);
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function save(array $form, FormStateInterface $form_state) {
    parent::save($form, $form_state);

    // Delete VBO temp storage.
    $this->deleteTempstoreData('[VIEW_NAME]', '[DISPLAY_ID]');
    $form_state->setRedirect('entity.delivery_note.canonical', ['delivery_note' => $entity->id()]);
  }

}

[VIEW_NAME] & [DISPLAY_ID] are the View machine name, and the Views display ID / name, where the VBO checkboxes where added.

graber’s picture

Status: Needs review » Fixed

Switching to fixed, please feel free to reopen if some additional improvements can be made to the API.

graber’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

graber’s picture

Please check #3207304: Allow to override the batch finished method. for a new API, when this is merged we'll be able to override the entire batch finished callback with redirection in any action class.