Drupal 8: Running A Batch Through An AJAX Request

20th September 2019

This problem came out of a recent project I was working on. I had to perform a bunch of API lookups on behalf of a user that could take a minute or so to complete. The results of the API lookups were cached and so once it was done the site would be very quick, unfortunately the initial API lookup slowed down the page load quite considerably so this created a problem.

Rather than just doing the API loading in the page load process and making the user sit through it I created a batch process to load the API results in a more manageable manner. This created another problem as although the batch runner in Drupal is really good, it is perhaps a little too much just to show to users and expect them to understand what is going on. This led me to think if I could run a batch process via an AJAX callback from the page they were trying to load.

I have to say that I searched for a solution to this problem for quite a while. Turns out that no one had solved this problem before (that I could see).

The first step in this was to create a library that would control the AJAX callback to the batch process.

  1. loader:
  2.   js:
  3.   js/loader.js: {}
  4.   dependencies:
  5. - core/jquery
  6. - core/drupalSettings

Along with the associated JavaScript file called loader.js. I didn't know what I needed to fill in here so just a stub, so I first created it with a minimal AJAX callback to a route that would trigger the batch process.

  1. (function ($, Drupal) {
  2. 'use strict';
  3.  
  4. Drupal.behaviors.account = {
  5. attach: function attach(context, settings) {
  6. $.ajax({
  7. url: Drupal.url('loading/ajax'),
  8. type: 'POST',
  9. contentType: 'application/json; charset=utf-8',
  10. dataType: 'json',
  11. success: function success(value) {
  12. // Do stuff...
  13. }
  14. });
  15. }
  16. };
  17. })(jQuery, Drupal);

This was loaded onto the page by attaching it to the output of the page. In this case I was just using a static interstitial page to run the AJAX request.

  1. public function myLoading() {
  2. return [
  3. '#theme' => 'my_loading',
  4. '#attached' => [
  5. 'library' => [
  6. 'my_module/loader',
  7. ],
  8. ],
  9. ];
  10. }

This was a standard Drupal controller so it's not doing anything special. This could potentially be in a block or something but we wanted to take the user to a 'loading' page whilst we grabbed things from the API and then send them back to the page they were trying to access.

The final step was to set up the AJAX endpoint so that it could trigger the batch process.

  1. public function ajaxBatchProcess() {
  2. // Setup batch process.
  3. $this->batchService->setupLengthyBatchProcess();
  4.  
  5. // Get the batch that we just created.
  6. $batch =& batch_get();
  7.  
  8. // Ensure that the finished response doesn't produce any messages.
  9. $batch['sets'][0]['finished'] = NULL;
  10.  
  11. // Create the batch_process(), and feed it a URL that it will go to.
  12. $url = Url::fromRoute('user.page');
  13. $response = batch_process($url);
  14.  
  15. // Return the response to the ajax output.
  16. $ajaxResponse = new AjaxResponse();
  17. return $ajaxResponse->addCommand(new BaseCommand('batchcustomer', $response->getTargetUrl()));
  18. }

I have missed out some of the complexity here, but essentially I had wrapped the batch creation in a service. This service was essentially a class that setup and managed the batch run. The function setupLengthyBatchProcess() essentially just wraps the batch_set($batch); call and can be used in a submit handler or something similar. The batch finish function also reported on what it had just completed so this was removed in order to prevent these messages being shown to the user.

After some investigation I found that the result of the batch_process() function is to return a path to the batch runner (e.g. batch?id=12345&op=start). I didn't want to show this to the user so I couldn't just return a redirect response to the AJAX request.

One way around this was to set the 'progressive' option in the batch settings. What this does is essentially process the entire batch in one go, which isn't really what I wanted to do. What we lose (aside from being able to handle the amount of data being processed in little chunks) is the ability to tell the user how long they have to wait as we can't report on how far through the batch process we are. A simple progress indicator is really useful, even if the user only has to wait 10 seconds.

If you do want to go down this progressive route then just change a few files in the above code and everything I have shown so far will work. Note that I'm no longer sending a URL to the batch_process() function as we aren't going to be doing any redirecting. You also need to make sure that when the AJAX request has finished that it redirects correctly as it currently does nothing.

  1. $batch =& batch_get();
  2.  
  3. $batch['progressive'] = TRUE;
  4.  
  5. $response = batch_process();

What I ended up doing was reverse engineering the batch runner that Drupal 8 has. This uses a plugin called ProgressBar that basically watches an endpoint and reports on the progress. the updateCallback() function is used to either update the user as to progress or perform a redirect. This effectively runs the batch process in the same way that Drupal would normally run it.

  1. (function ($, Drupal) {
  2. 'use strict';
  3.  
  4. Drupal.behaviors.account = {
  5. attach: function attach(context, settings) {
  6. var progressBar = void 0;
  7.  
  8. function updateCallback(progress, status, pb) {
  9. $('#updateprogress').html(progress + '%');
  10. if (progress === '100') {
  11. pb.stopMonitoring();
  12. window.location = '/user';
  13. }
  14. }
  15.  
  16. function errorCallback(pb) {
  17. }
  18.  
  19. $.ajax({
  20. url: Drupal.url('account/loading/ajax'),
  21. type: 'POST',
  22. contentType: 'application/json; charset=utf-8',
  23. dataType: 'json',
  24. success: function success(value) {
  25. progressBar = new Drupal.ProgressBar('updateprogress', updateCallback, 'POST', errorCallback);
  26. progressBar.startMonitoring(value[0].data + '&op=do', 10);
  27. }
  28. });
  29. }
  30. };
  31. })(jQuery, Drupal);

Ultimately, this works very well. When the users login (and we detect that they have no cache yet) they are sent to this intermediate page and the batch process is run behind the scenes whilst they wait for the API calls to complete. The process reports back how far it has got to finish and once complete the users are sent back to their account page.

Comments

Permalink

Τheгe is definitely lots to find οut about this topic. I love aⅼl the points you have made.

Submitted by Dementi on Sat, 10/05/2019 - 06:17

Add new comment

The content of this field is kept private and will not be shown publicly.