Profile picture for user admin
Daniel Sipos
03 Aug 2015

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.

Profile picture for user admin

Daniel Sipos

CEO @ Web Omelette

Danny founded WEBOMELETTE in 2012 as a passion project, mostly writing about Drupal problems he faced day to day, as well as about new technologies and things that he thought other developers would find useful. Now he now manages a team of developers and designers, delivering quality products that make businesses successful.

Contact us

Comments

Khoa Pham 03 Aug 2015 10:08

Thanks for your post, It's

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

zachary 06 Aug 2015 01:59

need module support?

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

thanks

Daniel Sipos 06 Aug 2015 10:00

In reply to by zachary (not verified)

Well the action definition is

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 :)

Mohammed 27 Apr 2016 14:51

I actually want to create a

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

dr e.love per 16 May 2016 14:08

tried this and not working..

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
}

Girish Cholayill 06 Nov 2016 11:09

Where do we input this code in VBO Module

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

W.M. 04 Dec 2016 11:51

Collecting data across _action(&$entity, $context) function

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.

Miguel 08 Apr 2020 20:29

Thanks!

Dude, thanks a lot for this!

I was building a custom bulk script when I found your article: way cleaner solution!
I'm definitely using this instead.

Thanks

S.G. 16 Oct 2020 03:20

Thank you!!!

After all day of fighting with Rules and Components, I found this. This is Goldmine! It opens up so many other possibilities.

Tim 14 Dec 2020 17:14

If new operation does not show up in the operations menu

I found that I needed to edit the "Bulk operations" field settings on the system content view and add my new action to the list of displayed actions before it would show up in the menu.

Add new comment