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.
Daniel Sipos
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.
Comments
Thanks for your post, It's
Thanks for your post, It's really helpful for me
need module support?
Do this need to install VBO module for using the code to implement the feature?
thanks
In reply to need module support? 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 :)
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
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
}
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
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.In reply to Collecting data across _action(&$entity, $context) function by W.M. (not verified)
I am experimenting now with
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.
In reply to Collecting data across _action(&$entity, $context) function by W.M. (not verified)
If you need all selected
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.
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
Thank you!!!
After all day of fighting with Rules and Components, I found this. This is Goldmine! It opens up so many other possibilities.
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