Need some help with your project? Contact me

Write your own Views Bulk Operations actions in Drupal 7

Views Bulk Operations (VBO) is a powerful module that leverages Views to allow site administrators to perform bulk operations on multiple entities at once. It does so efficiently by processing the items in batches over multiple requests to avoid timeouts.

Installing the module will already provide you with a host of various actions you can perform in bulk on various entities. You can publish 1000 nodes at once, delete them or even change their author. And these are just a few examples of what you can do.

In this article we are going to look at programatically creating our own action that we can trigger with VBO to affect multiple node entities. Sometimes you need to write your own because the use case does not quite fit in the extensive list of actions the module provides.

For our example, let's assume we have an entity reference field called field_users on both the article and basic page content types. This field can reference whatever user it wants. And the requirement is to be able to bulk update the value of this field on a bunch of nodes of both these node types at once.

Out of the box, VBO provides us with an action to change the value of a field but this doesn't help us in this case. When adding a value to this field via VBO, we are presented with as many instances of the field as different node types are in the selection. And this is not ideal if we want to scale the functionality to more than one content type. What we want is to select a number of nodes and then only once provide a value to this field. So let's see how we can define a custom VBO action for this use case.

The action

To define a new action we need to implement hook_action_info():

/**
 * Implements hook_action_info().
 */
function my_module_action_info() {
  return array(
    'my_module_my_custom_action' => array(
      'type' => 'entity',
      'label' => t('Add a user to Users field'),
      'behavior' => array('changes_property'),
      'configurable' => TRUE,
      'vbo_configurable' => FALSE,
      'triggers' => array('any'),
    ),
  );
}

With this hook implementation we are defining our own action called my_module_my_custom_action which is available to be triggered on all entity types (because we specified entity for the type) and it acts as a property changer. It is configurable using the default Action API but we don't need any kind of VBO specific configuration. For more information on all the values that you can pass here, feel free to consult the documentation page for VBO.

Next, it's time to create the configuration form for this action, namely the form that will be presented to us to select the user we want to add to the field_users reference field:

function my_module_my_custom_action_form() {
  $form = array();
  $form['user'] = array(
    '#type' => 'textfield',
    '#title' => t('User'),
    '#maxlength' => 60,
    '#autocomplete_path' => 'user/autocomplete',
    '#weight' => -1,
  );

  return $form;
}

The function name takes from the machine name of the action suffixed by _form and is responsible for creating and returning a form array. All we need is one field which uses the core user/autocomplete path to load users via Ajax. Simple enough.

So now after we make a bulk selection and choose our action, we'll be prompted with this form to choose the user we want to add to the reference field. It follows to couple it with a submit handler that will save the value into the context of the operation:

function my_module_my_custom_action_submit($form, &$form_state) {
  $uid = db_query('SELECT uid from {users} WHERE name = :name', array(':name' => $form_state['values']['user']))->fetchField();
  return array(
    'uid' => $uid,
  );
}

The naming of this function is similar to the previous one except for the suffix being _submit this time around. In it, we load from the database the uid of the user that was referenced in the form field by name and return that inside an array. The latter will then be merged into the $context variable available in the next step.

So it's now time to write the final function which represents this step by adding the selected user to the existing ones in that field across all the selected nodes, regardless of their type:

function my_module_my_custom_action(&$entity, $context) {
  if (!isset($entity->field_users)) {
    return;
  }

  if (!isset($context['uid'])) {
    return;
  }

  if (!empty($entity->field_users)) {
    foreach ($entity->field_users[LANGUAGE_NONE] as $ref) {
      if ($ref['target_id'] === $context['uid']) {
        return;
      }
    }
  }

  $user =  array(
    'target_id' => $context['uid'],
  );

  if (!empty($entity->field_users)) {
    $entity->field_users[LANGUAGE_NONE][] = $user;
    return;
  }

  $entity->field_users[LANGUAGE_NONE] = array($user);
}

The name of this function is exactly the same as the machine name of the action, the reason for which we prefixed the latter with the module name. As arguments, this function gets the entity object that is being changed (by reference) and the context of the operation.

We start by returning early if the current entity doesn't have our field_users field or if by any chance the uid key is not available inside $context. Then we loop through all the values of the field and return if the selected uid already exists (we don't want to add it twice). And last, we add the selected uid to the list of existing users in the field by taking into account the possibilities that the field can be empty or it can already contain values. After passing through this action, VBO will automatically save the node with the changes for us.

And that is pretty much it. Clearing the cache will make the new action available in the VBO configuration of your view. Adding it will then allow you to select as many nodes as you want, specify a user via the autocomplete field and have that user be added to the field_users field of all those nodes. And the cool thing is that you can select any node you want: if the field doesn't exist on that content type, it will just be skipped gracefully because we are checking for this inside the action logic.

Hope this helps.

Comments

Thanks for your post, It's really helpful for me

Do this need to install VBO module for using the code to implement the feature?

thanks

Well the action definition is Drupal core so you could then also use them without VBO somehow programatically by triggering the action, loading the form etc. But the whole point is to use it with VBO. In which case, yes, you need VBO :)

I actually want to create a SINGLE node from data collected from several row in views. Precisely I would like to put that data on a multiple valued field collection. Can you please suggest some starting point. Thanks

tried this and not working..

function xyz_action_info() {
return array(
'xyz_my_custom_action' => array(
'type' => 'entity',
'label' => t('App off'),
'behavior' => array('changes_property'),
'configurable' => TRUE,
'vbo_configurable' => FALSE,
'triggers' => array('any'),
),
);
}

function xyz_my_custom_action_form() {
$form = array();
$form['sao_cb'] = array(
'#type' => 'checkbox',
'#title' => t('User'),
);

return $form;
}

function xyz_my_custom_action_submit($form, &$form_state) {
return array(
$form_state['values']['sao_cb'] => $form_state['values']['sao_cb'],
);
}

function xyz_my_custom_action(&$entity, $context) {
var_dump($entity); exit;
// also tried
drupal_set_message(print_r($entity, TRUE)); // still the same no effect
}

Hi Danny, Thank you for the post. Could you guide me as to where do I input the custom codes for VBO actions? I am learning drupal development, however, I could not find this information anywhere.

Thanks

Hello Danny, thank you for this great tutorial. I have a question that I hope you can help me with. I am trying to collect data from node fields, gather them all and then process them. In other words, the action I am planning to do is not entity based but whole-set based. My question, how to collect those pieces of data across the function function my_module_my_custom_action(&$entity, $context) {and then after it finishes all iterations, to work on the data. What would you suggest? Thank you.

I am experimenting now with storing the aggregated data in a PHP session variable.. I will see if that passes the test of a real world situation.

If you need all selected entities at once in your action callback, just add 'aggregate' => TRUE in hook_action_info(). Your callback function will get an array of entity objects instead of a single one.

Add new comment

You can post comments in Markdown and basic HTML tags.
For code blocks, wrap your code within '~~~'. For example:
~~~
$var = 'my variable';
~~~