Score:2

Deleting entities using Batch API without a form or user interface

us flag

In a custom Drupal 9 module, I need to delete entities using Batch API without a form or user interface. The list of entity IDs are generated and the batch is built in hook_views_post_execute(). When the View path is accessed, I can confirm the batch is successfully created by noting the new record in the "batch" database table. However, the batch is never processed and none of the entities are deleted. Running cron has no effect.

I'm obviously missing the mechanism to execute the batch. How does the batch get processed repeatedly until all the entities are deleted?

Again, I just want the batch to process transparently. For this reason, I suspect the batch message properties don't need to be defined.

use Drupal\views\ViewExecutable;
use Drupal\Core\Batch\BatchBuilder;

/**
 * Implements hook_views_post_execute().
 */
function custom_views_post_execute(ViewExecutable $view) {
  if ($view->id() == 'results_delete_by_teacher' && $view->current_display == 'results_delete') {
    if (count($view->result)) {

      // Create a new batch.
      $batchBuilder = new BatchBuilder();
      $batchBuilder->setTitle('Deleting entities')
        ->setFinishCallback('custom_batch_delete_entities_finished')
        ->setInitMessage('Starting to delete entities.')
        ->setProgressMessage('Deleted @current out of @total entities.')
        ->setErrorMessage('An error occurred while deleting entities.');

      foreach ($view->result as $id => $result) {
        $batchBuilder->addOperation('custom_batch_delete_entity', ['result', $result->id]);
        $batchBuilder->addOperation('custom_batch_delete_entity', ['file', $result->result_field_data_id]);
      }

      // Start the batch.
      batch_set($batchBuilder->toArray());
      return batch_process('user');
    }
  }
}

/**
 * Batch operation callback to delete an entity.
 */
function custom_batch_delete_entity($type, $entity_id, &$context) {
  // Load and delete the result entity.
  $entity = \Drupal::entityTypeManager()->getStorage($type)->load($entity_id);
  $entity->delete();

  // Update the progress information.
  $context['message'] = t('Deleted entity with ID @id', ['@id' => $entity_id]);
  $context['results'][] = $entity_id;
}

/**
 * Batch finished callback.
 */
function custom_batch_delete_entities_finished($success, $results, $operations) {
  if ($success) {
    \Drupal::logger('custom')->notice('All entities deleted successfully.');
  }
  else {
    \Drupal::logger('custom')->error('An error occurred during entity deletion.');
  }
}
id flag
Use a queue not a batch.
leymannx avatar
ne flag
+1 for Queue API.
bacteriaman avatar
us flag
Thanks for the quick replies! I will investigate Queue API and report back.
leymannx avatar
ne flag
Please ask a new question instead of making the existing question a new question which doesn't match the answers anymore. Thank you
Score:3
us flag

Without a page (which could also contain a form) rendered in a browser, the batch API operations do not work, since it is the browser that calls the URL which makes the batch progress.

As the comments said, the way to go is using the queue API via a @QueueWorker plugin, which is invoked when cron tasks run. This means you cannot expect that all the items in the queue are quickly handled, especially when there are many queued items, but this does not depend from a browser that initializes the batch API operations and stay connected to the server for all the time required from the batch API operations. (If the browser loses the connection with the server, the batch API operations are not completed.)

This requires to define a plugin class that uses the @QueueWorker annotation, which must use the Drupal\[module_machine_name]\Plugin\QueueWorker namespace ([module_machine_name] must be replaced by the module machine name, for example aggregator for the Aggregator module). The plugin class, which usually extends Drupal\Core\Queue\QueueWorkerBase, must define the processItem() method, as requested by Drupal\Core\Queue\QueueWorkerInterface, which receives the items added to the queue, one at time. The plugin ID is also the ID for the queue used for the items.

The hook that adds items to the queue just needs to get the queue with $queue = \Drupal::queue('[queue_name]'); (replace '[queue_name]' with the string containing the queue name) and add items to the queue with $queue->createItem($object_or_array);.

As concrete example, see what the Aggregator module does. It adds items in the queue in a hook_cron() implementation, but that can be done on any hook implementation, or even a method or a function that is not a hook implementation.

aggregator.module

function aggregator_cron() {
  $queue = \Drupal::queue('aggregator_feeds');
  $request_time = \Drupal::time()->getRequestTime();
  $ids = \Drupal::entityTypeManager()->getStorage('aggregator_feed')->getFeedIdsToRefresh();
  foreach (Feed::loadMultiple($ids) as $feed) {
    if ($queue->createItem($feed)) {
      $feed->setQueuedTime($request_time);
      $feed->save();
    }
  }

  $ids = \Drupal::entityQuery('aggregator_feed')
    ->accessCheck(FALSE)
    ->condition('queued', $request_time - 3600 * 6, '<')
    ->execute();
  if ($ids) {
    $feeds = Feed::loadMultiple($ids);
    foreach ($feeds as $feed) {
      $feed->setQueuedTime(0);
      $feed->save();
    }
  }
}

core/modules/aggregator/src/Plugin/QueueWorker/AggregatorRefresh.php

namespace Drupal\aggregator\Plugin\QueueWorker;

use Drupal\aggregator\FeedInterface;
use Drupal\Core\Queue\QueueWorkerBase;

/**
 * Updates a feed's items.
 *
 * @QueueWorker(
 *   id = "aggregator_feeds",
 *   title = @Translation("Aggregator refresh"),
 *   cron = {"time" = 60}
 * )
 */
class AggregatorRefresh extends QueueWorkerBase {

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    if ($data instanceof FeedInterface) {
      $data->refreshItems();
    }
  }

}

As clear from the code, items added to the queue can also be objects. The processItem() method will receive them in its $data parameter, and it will be able to call any public method they have.

bacteriaman avatar
us flag
I have updated my original post to include code for using Queue API. However, the queued items are still not processed when running cron. I guess I need to employ the QueueWorker plugin, like the aggregator example shows.
apaderno avatar
us flag
`hook_cron_queue_info()` is not used in Drupal 8, which uses plugins.
Score:0
us flag

Thanks to @apaderno for his explanation of Queue API.

Here's the working code for my particular need, which is queuing entities for deletion from Views results. In my case, I want to delete a custom entity and the file entity reference. Once the View has executed, the "queue" database table contains the corresponding number of results. Running cron will proceed to delete the records and physical files as expected.

modules/custom/custom/custom.module

/**
 * Implements hook_views_post_execute().
 */
function custom_views_post_execute(ViewExecutable $view) {
  if ($view->id() == 'results_delete_by_teacher' && $view->current_display == 'results_delete') {
    if (count($view->result)) {
      $queue = \Drupal::queue('custom_entity_delete');

      foreach ($view->result as $id => $result) {
        // Add the entities to the deletion queue.
        $queue->createItem([
          'entity_type' => 'result',
          'entity_id' => $result->id,
        ]);
        $queue->createItem([
          'entity_type' => 'file',
          'entity_id' => $result->result_field_data_id,
        ]);
      }

      \Drupal::logger('custom')->notice('Queued @count result entities for deletion.', ['@count' => count($view->result)]);
    }
  }
}

modules/custom/custom/src/Plugin/QueueWorker/customEntityDelete.php

namespace Drupal\custom\Plugin\QueueWorker;

use Drupal\Core\Queue\QueueWorkerBase;

/**
 * Provides a QueueWorker for deleting entities.
 *
 * @QueueWorker(
 *   id = "custom_entity_delete",
 *   title = @Translation("Entity delete queue"),
 *   cron = {"time" = 60}
 * )
 */
class customEntityDelete extends QueueWorkerBase {

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    // Load and delete the entity.
    $entity = \Drupal::entityTypeManager()->getStorage($data['entity_type'])->load($data['entity_id']);

    if ($entity) {
      $entity->delete();
      \Drupal::logger('custom')->notice('Deleted entity @type with ID: @id', ['@type' => $data['entity_type'], '@id' => $data['entity_id']]);
    }
  }
}

modules/custom/custom/custom.services.yml

services:
  custom.entitiy_delete:
    class: Drupal\custom\Plugin\QueueWorker\customEntityDelete
    tags:
      - { name: queue_worker }
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.