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.