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.