How to create a Views bulk operation
Views bulk operations are a great tool for mass content operations in Drupal, and creating a bulk action is pretty simple.
For this post, we’ll set ourselves the task of creating a bulk action for adding tags to the selected nodes. I have created a basic installation for my Drupal 9 Code Examples, and we’ll be using this as a baseline.
Disclaimer this example does not aim to meet all possible implementations but is targeted at the specific installation. This is not contrib, it’s custom.
Annotation
**
* VBO Action for adding tags to nodes.
*
* @Action(
* id = "vbo_examples_add_tags_to_node",
* label = @Translation("Add Tags to Node"),
* type = "node"
* )
*/
Annotations have become the backbone of plugins for Drupal 9, and VBO makes use of the @Action annotation implemented in Drupal core (@see Drupal\Core\Annotation\Action).
For this example, we use the most basic form by adding an id
and label
to identify our action. The id
should be unique. It’s good practice to prefix your action with the module name because it makes it easier to ensure its uniqueness.
Next, we’ll set the type
to node, to tell the action system we only want to apply the action to nodes. As stated, we’re only interested in adding tags to nodes.
Class definition
class AddTagsToNodeAction extends ViewsBulkOperationsActionBase implements PluginFormInterface {}
Naming here is easy. We name it AddTagsToNode
because that’s what the action does. Naming things after what they do always makes sense because it makes it easier to identify a class.
Next, we place it in the src/Plugin/Action
directory of our module, and namespace it with Drupal\vbo_examples\Plugin\Action
(Drupal\{module_name}\Plugin\Action
).
The plugin system will look for it in this directory, so placing the action there is important. We’ll extend the ViewsBulkOperationsActionBase
class and implement the PluginFormInterface
as required for actions with user input, but more on that shortly.
User input
For user input, we’ll add a taxonomy field where the user can find taxonomy terms (tags) to add to the selected nodes. Here we’ll need the PluginFormInterface
methods buildConfigurationForm
and submitConfigurationForm
.
To the first method, we’ll add a form field with the type entity_autocomplete
and target type taxonomy_term
as we’ll be looking for terms. Under selection settings, we’ll specify that we need to look up only terms from the tags
vocabulary. If you are unfamiliar with the entity_autocomplete form element you can find more information here.
Upon submitting this form, we’ll update the action config to use as storage for the selected terms. This is a form of temporary storage for the action, and can later be accessed during the execute method.
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['new_tags'] = [
'#title' => $this->t('New Tags'),
'#type' => 'entity_autocomplete',
'#target_type' => 'taxonomy_term',
'#tags' => TRUE,
'#selection_settings' => [
'target_bundles' => ['tags'],
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['new_tags'] = $form_state->getValue('new_tags');
}
Execute/ExecuteMultiple
Next, we’ll need to implement the execute
method, where we add the tags to our node entity. You can override the executeMultiple
method as well, but by default, this iterates over the chosen nodes and runs execute
on them.
Since we’re not doing anything fancy in this example, we’ll simply implement the execute method. It only has entity
as an argument, and on it, we’ll need to do a little validation.
We need to ensure the node has the correct field (in this case field_tags
), so we’ll start by checking the entity is a Node (we already set the type, so this just helps with code completion in my IDE) and use the hasField
method to check if the field is present.
Next, we’ll need to make sure the node only has each tag once (unique tags), and then we’ll add the tags as relations to field_tags
and save the node.
/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
if ($entity instanceof Node) {
if ($entity->hasField('field_tags')) {
$tags = $entity->get('field_tags')->getValue();
$flat_tags = [];
foreach ($tags as $tag) {
$flat_tags[] = $tag['target_id'];
}
foreach ($this->configuration['new_tags'] as $new_tag) {
$flat_tags[] = $new_tag['target_id'];
}
$flat_tags = array_unique($flat_tags);
$new_tags = [];
foreach ($flat_tags as $flat_tag) {
$new_tags[] = ['target_id' => $flat_tag];
}
$entity->set('field_tags', $new_tags);
$entity->save();
return $this->t('Tags were added to the node.');
}
else {
return $this->t('Node %type does not have tags.', ['%type' => $entity->bundle()]);
}
}
return $this->t('Action can only be performed on nodes.');
}
Access checks
The last method we need to look at is access
. As you may have guessed it’s used to determine if a user has access to perform a certain operation on an object.
In this case, we simply check if the user is allowed to update a node. If not, the bulk operation will fail.
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\Core\Access\AccessResultInterface $result */
$result = $object->access('update', $account, TRUE);
return $return_as_object ? $result : $result->isAllowed();
}
Adding bulk actions to views
To use your new bulk action, you need a view with the field Global: Views bulk operations
added. You can edit the standard /admin/content view if you wish. I have done so in the example project, to avoid having to make a custom view.
VBO Actions don’t show up in the Node operations bulk form
, and I’m not sure why. But until I figure it out, it works with the global field.