diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index de43364..38dfd56 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -14,11 +14,7 @@
* @see batch_get()
*/
-use Drupal\Component\Utility\Timer;
-use Drupal\Component\Utility\UrlHelper;
-use Drupal\Core\Batch\Percentage;
-use Drupal\Core\Form\FormState;
-use Drupal\Core\Url;
+use Drupal\Core\Batch\BatchQueueController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -30,6 +26,8 @@
* The current request object.
*
* @see _batch_shutdown()
+ *
+ * @internal
*/
function _batch_page(Request $request) {
$batch = &batch_get();
@@ -115,6 +113,8 @@ function _batch_page(Request $request) {
*
* @return bool
* TRUE if the batch information needs to be updated; FALSE otherwise.
+ *
+ * @internal
*/
function _batch_needs_update($new_value = NULL) {
$needs_update = &drupal_static(__FUNCTION__, FALSE);
@@ -131,18 +131,27 @@ function _batch_needs_update($new_value = NULL) {
*
* @see _batch_progress_page_js()
* @see _batch_process()
+ *
+ * @internal
*/
function _batch_do() {
// Perform actual processing.
list($percentage, $message, $label) = _batch_process();
- return new JsonResponse(['status' => TRUE, 'percentage' => $percentage, 'message' => $message, 'label' => $label]);
+ return new JsonResponse([
+ 'status' => TRUE,
+ 'percentage' => $percentage,
+ 'message' => $message,
+ 'label' => $label,
+ ]);
}
/**
* Outputs a batch processing page.
*
* @see _batch_process()
+ *
+ * @internal
*/
function _batch_progress_page() {
$batch = &batch_get();
@@ -221,7 +230,8 @@ function _batch_progress_page() {
'batch_progress_meta_refresh',
],
],
- // Adds JavaScript code and settings for clients where JavaScript is enabled.
+ // Adds JavaScript code and settings for clients where JavaScript is
+ // enabled.
'drupalSettings' => [
'batch' => [
'errorMessage' => $current_set['error_message'] . '
' . $batch['error_message'],
@@ -247,164 +257,20 @@ function _batch_progress_page() {
*
* @return array
* An array containing a completion value (in percent) and a status message.
- */
-function _batch_process() {
- $batch = &batch_get();
- $current_set = &_batch_current_set();
- // Indicate that this batch set needs to be initialized.
- $set_changed = TRUE;
-
- // If this batch was marked for progressive execution (e.g. forms submitted by
- // \Drupal::formBuilder()->submitForm(), initialize a timer to determine
- // whether we need to proceed with the same batch phase when a processing time
- // of 1 second has been exceeded.
- if ($batch['progressive']) {
- Timer::start('batch_processing');
- }
-
- if (empty($current_set['start'])) {
- $current_set['start'] = microtime(TRUE);
- }
-
- $queue = _batch_queue($current_set);
-
- while (!$current_set['success']) {
- // If this is the first time we iterate this batch set in the current
- // request, we check if it requires an additional file for functions
- // definitions.
- if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) {
- include_once \Drupal::root() . '/' . $current_set['file'];
- }
-
- $task_message = $label = '';
- // Assume a single pass operation and set the completion level to 1 by
- // default.
- $finished = 1;
-
- if ($item = $queue->claimItem()) {
- list($callback, $args) = $item->data;
-
- // Build the 'context' array and execute the function call.
- $batch_context = [
- 'sandbox' => &$current_set['sandbox'],
- 'results' => &$current_set['results'],
- 'finished' => &$finished,
- 'message' => &$task_message,
- ];
- call_user_func_array($callback, array_merge($args, [&$batch_context]));
-
- if ($finished >= 1) {
- // Make sure this step is not counted twice when computing $current.
- $finished = 0;
- // Remove the processed operation and clear the sandbox.
- $queue->deleteItem($item);
- $current_set['count']--;
- $current_set['sandbox'] = [];
- }
- }
-
- // When all operations in the current batch set are completed, browse
- // through the remaining sets, marking them 'successfully processed'
- // along the way, until we find a set that contains operations.
- // _batch_next_set() executes form submit handlers stored in 'control'
- // sets (see \Drupal::service('form_submitter')), which can in turn add new
- // sets to the batch.
- $set_changed = FALSE;
- $old_set = $current_set;
- while (empty($current_set['count']) && ($current_set['success'] = TRUE) && _batch_next_set()) {
- $current_set = &_batch_current_set();
- $current_set['start'] = microtime(TRUE);
- $set_changed = TRUE;
- }
-
- // At this point, either $current_set contains operations that need to be
- // processed or all sets have been completed.
- $queue = _batch_queue($current_set);
-
- // If we are in progressive mode, break processing after 1 second.
- if ($batch['progressive'] && Timer::read('batch_processing') > 1000) {
- // Record elapsed wall clock time.
- $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
- break;
- }
- }
-
- if ($batch['progressive']) {
- // Gather progress information.
-
- // Reporting 100% progress will cause the whole batch to be considered
- // processed. If processing was paused right after moving to a new set,
- // we have to use the info from the new (unprocessed) set.
- if ($set_changed && isset($current_set['queue'])) {
- // Processing will continue with a fresh batch set.
- $remaining = $current_set['count'];
- $total = $current_set['total'];
- $progress_message = $current_set['init_message'];
- $task_message = '';
- }
- else {
- // Processing will continue with the current batch set.
- $remaining = $old_set['count'];
- $total = $old_set['total'];
- $progress_message = $old_set['progress_message'];
- }
-
- // Total progress is the number of operations that have fully run plus the
- // completion level of the current operation.
- $current = $total - $remaining + $finished;
- $percentage = _batch_api_percentage($total, $current);
- $elapsed = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0;
- $values = [
- '@remaining' => $remaining,
- '@total' => $total,
- '@current' => floor($current),
- '@percentage' => $percentage,
- '@elapsed' => \Drupal::service('date.formatter')->formatInterval($elapsed / 1000),
- // If possible, estimate remaining processing time.
- '@estimate' => ($current > 0) ? \Drupal::service('date.formatter')->formatInterval(($elapsed * ($total - $current) / $current) / 1000) : '-',
- ];
- $message = strtr($progress_message, $values);
- if (!empty($task_message)) {
- $label = $task_message;
- }
-
- return [$percentage, $message, $label];
- }
- else {
- // If we are not in progressive mode, the entire batch has been processed.
- return _batch_finished();
- }
-}
-
-/**
- * Formats the percent completion for a batch set.
- *
- * @param int $total
- * The total number of operations.
- * @param int|float $current
- * The number of the current operation. This may be a floating point number
- * rather than an integer in the case of a multi-step operation that is not
- * yet complete; in that case, the fractional part of $current represents the
- * fraction of the operation that has been completed.
- *
- * @return string
- * The properly formatted percentage, as a string. We output percentages
- * using the correct number of decimal places so that we never print "100%"
- * until we are finished, but we also never print more decimal places than
- * are meaningful.
*
- * @see _batch_process()
+ * @internal
*/
-function _batch_api_percentage($total, $current) {
- return Percentage::format($total, $current);
+function _batch_process() {
+ return BatchQueueController::processQueue();
}
/**
* Returns the batch set being currently processed.
+ *
+ * @internal
*/
function &_batch_current_set() {
- $batch = &batch_get();
- return $batch['sets'][$batch['current_set']];
+ return BatchQueueController::getCurrentSet();
}
/**
@@ -417,20 +283,11 @@ function &_batch_current_set() {
* @return true|null
* TRUE if a subsequent set was found in the batch; no value will be returned
* if no subsequent set was found.
+ *
+ * @internal
*/
function _batch_next_set() {
- $batch = &batch_get();
- if (isset($batch['sets'][$batch['current_set'] + 1])) {
- $batch['current_set']++;
- $current_set = &_batch_current_set();
- if (isset($current_set['form_submit']) && ($callback = $current_set['form_submit']) && is_callable($callback)) {
- // We use our stored copies of $form and $form_state to account for
- // possible alterations by previous form submit handlers.
- $complete_form = &$batch['form_state']->getCompleteForm();
- call_user_func_array($callback, [&$complete_form, &$batch['form_state']]);
- }
- return TRUE;
- }
+ return BatchQueueController::nextSet();
}
/**
@@ -438,110 +295,11 @@ function _batch_next_set() {
*
* Call the 'finished' callback of each batch set to allow custom handling of
* the results and resolve page redirection.
+ *
+ * @internal
*/
function _batch_finished() {
- $batch = &batch_get();
- $batch_finished_redirect = NULL;
-
- // Execute the 'finished' callbacks for each batch set, if defined.
- foreach ($batch['sets'] as $batch_set) {
- if (isset($batch_set['finished'])) {
- // Check if the set requires an additional file for function definitions.
- if (isset($batch_set['file']) && is_file($batch_set['file'])) {
- include_once \Drupal::root() . '/' . $batch_set['file'];
- }
- if (is_callable($batch_set['finished'])) {
- $queue = _batch_queue($batch_set);
- $operations = $queue->getAllItems();
- $batch_set_result = call_user_func_array($batch_set['finished'], [$batch_set['success'], $batch_set['results'], $operations, \Drupal::service('date.formatter')->formatInterval($batch_set['elapsed'] / 1000)]);
- // If a batch 'finished' callback requested a redirect after the batch
- // is complete, save that for later use. If more than one batch set
- // returned a redirect, the last one is used.
- if ($batch_set_result instanceof RedirectResponse) {
- $batch_finished_redirect = $batch_set_result;
- }
- }
- }
- }
-
- // Clean up the batch table and unset the static $batch variable.
- if ($batch['progressive']) {
- \Drupal::service('batch.storage')->delete($batch['id']);
- foreach ($batch['sets'] as $batch_set) {
- if ($queue = _batch_queue($batch_set)) {
- $queue->deleteQueue();
- }
- }
- // Clean-up the session. Not needed for CLI updates.
- if (isset($_SESSION)) {
- unset($_SESSION['batches'][$batch['id']]);
- if (empty($_SESSION['batches'])) {
- unset($_SESSION['batches']);
- }
- }
- }
- $_batch = $batch;
- $batch = NULL;
-
- // Redirect if needed.
- if ($_batch['progressive']) {
- // Revert the 'destination' that was saved in batch_process().
- if (isset($_batch['destination'])) {
- \Drupal::request()->query->set('destination', $_batch['destination']);
- }
-
- // Determine the target path to redirect to. If a batch 'finished' callback
- // returned a redirect response object, use that. Otherwise, fall back on
- // the form redirection.
- if (isset($batch_finished_redirect)) {
- return $batch_finished_redirect;
- }
- elseif (!isset($_batch['form_state'])) {
- $_batch['form_state'] = new FormState();
- }
- if ($_batch['form_state']->getRedirect() === NULL) {
- $redirect = $_batch['batch_redirect'] ?: $_batch['source_url'];
- // Any path with a scheme does not correspond to a route.
- if (!$redirect instanceof Url) {
- $options = UrlHelper::parse($redirect);
- if (parse_url($options['path'], PHP_URL_SCHEME)) {
- $redirect = Url::fromUri($options['path'], $options);
- }
- else {
- $redirect = \Drupal::pathValidator()->getUrlIfValid($options['path']);
- if (!$redirect) {
- // Stay on the same page if the redirect was invalid.
- $redirect = Url::fromRoute('');
- }
- $redirect->setOptions($options);
- }
- }
- $_batch['form_state']->setRedirectUrl($redirect);
- }
-
- // Use \Drupal\Core\Form\FormSubmitterInterface::redirectForm() to handle
- // the redirection logic.
- $redirect = \Drupal::service('form_submitter')->redirectForm($_batch['form_state']);
- if (is_object($redirect)) {
- return $redirect;
- }
-
- // If no redirection happened, redirect to the originating page. In case the
- // form needs to be rebuilt, save the final $form_state for
- // \Drupal\Core\Form\FormBuilderInterface::buildForm().
- if ($_batch['form_state']->isRebuilding()) {
- $_SESSION['batch_form_state'] = $_batch['form_state'];
- }
- $callback = $_batch['redirect_callback'];
- $_batch['source_url']->mergeOptions(['query' => ['op' => 'finish', 'id' => $_batch['id']]]);
- if (is_callable($callback)) {
- $callback($_batch['source_url'], $_batch['source_url']->getOption('query'));
- }
- elseif ($callback === NULL) {
- // Default to RedirectResponse objects when nothing specified.
- return new RedirectResponse($_batch['source_url']->setAbsolute()->toString());
- }
- }
+ return BatchQueueController::finishedProcessing();
}
/**
@@ -549,6 +307,8 @@ function _batch_finished() {
*
* @see _batch_page()
* @see drupal_register_shutdown_function()
+ *
+ * @internal
*/
function _batch_shutdown() {
if (($batch = batch_get()) && _batch_needs_update()) {
diff --git a/core/includes/form.inc b/core/includes/form.inc
index 052c9ce..510b72c 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -6,11 +6,12 @@
*/
use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Batch\Batch;
+use Drupal\Core\Batch\BatchQueueController;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\RenderElement;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
-use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Prepares variables for select element templates.
@@ -664,9 +665,10 @@ function template_preprocess_form_element_label(&$variables) {
* 'finished' callback. Batch sets are processed sequentially, with the progress
* bar starting afresh for each new set.
*
- * @param $batch_definition
- * An associative array defining the batch, with the following elements (all
- * are optional except as noted):
+ * @param \Drupal\Core\Batch\Batch|array $batch_definition
+ * An object representing the batch or an associative array defining the
+ * batch. If an array is used then it has the following elements (all are
+ * optional except as noted):
* - operations: (required) Array of operations to be performed, where each
* item is an array consisting of the name of an implementation of
* callback_batch_operation() and an array of parameter.
@@ -713,58 +715,10 @@ function template_preprocess_form_element_label(&$variables) {
* TRUE, or to BatchMemory if progressive is FALSE.
*/
function batch_set($batch_definition) {
- if ($batch_definition) {
- $batch =& batch_get();
-
- // Initialize the batch if needed.
- if (empty($batch)) {
- $batch = [
- 'sets' => [],
- 'has_form_submits' => FALSE,
- ];
- }
-
- // Base and default properties for the batch set.
- $init = [
- 'sandbox' => [],
- 'results' => [],
- 'success' => FALSE,
- 'start' => 0,
- 'elapsed' => 0,
- ];
- $defaults = [
- 'title' => t('Processing'),
- 'init_message' => t('Initializing.'),
- 'progress_message' => t('Completed @current of @total.'),
- 'error_message' => t('An error has occurred.'),
- ];
- $batch_set = $init + $batch_definition + $defaults;
-
- // Tweak init_message to avoid the bottom of the page flickering down after
- // init phase.
- $batch_set['init_message'] .= '
';
-
- // The non-concurrent workflow of batch execution allows us to save
- // numberOfItems() queries by handling our own counter.
- $batch_set['total'] = count($batch_set['operations']);
- $batch_set['count'] = $batch_set['total'];
-
- // Add the set to the batch.
- if (empty($batch['id'])) {
- // The batch is not running yet. Simply add the new set.
- $batch['sets'][] = $batch_set;
- }
- else {
- // The set is being added while the batch is running. Insert the new set
- // right after the current one to ensure execution order, and store its
- // operations in a queue.
- $index = $batch['current_set'] + 1;
- $slice1 = array_slice($batch['sets'], 0, $index);
- $slice2 = array_slice($batch['sets'], $index);
- $batch['sets'] = array_merge($slice1, [$batch_set], $slice2);
- _batch_populate_queue($batch, $index);
- }
+ if (!($batch_definition instanceof Batch)) {
+ $batch_definition = Batch::createFromArray($batch_definition);
}
+ $batch_definition->enqueue();
}
/**
@@ -789,7 +743,7 @@ function batch_set($batch_definition) {
* @param \Drupal\Core\Url $url
* (optional) URL of the batch processing page. Should only be used for
* separate scripts like update.php.
- * @param $redirect_callback
+ * @param string|null $redirect_callback
* (optional) Specify a function to be called to redirect to the progressive
* processing page.
*
@@ -797,84 +751,7 @@ function batch_set($batch_definition) {
* A redirect response if the batch is progressive. No return value otherwise.
*/
function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = NULL) {
- $batch =& batch_get();
-
- if (isset($batch)) {
- // Add process information
- $process_info = [
- 'current_set' => 0,
- 'progressive' => TRUE,
- 'url' => isset($url) ? $url : Url::fromRoute('system.batch_page.html'),
- 'source_url' => Url::fromRouteMatch(\Drupal::routeMatch())->mergeOptions(['query' => \Drupal::request()->query->all()]),
- 'batch_redirect' => $redirect,
- 'theme' => \Drupal::theme()->getActiveTheme()->getName(),
- 'redirect_callback' => $redirect_callback,
- ];
- $batch += $process_info;
-
- // The batch is now completely built. Allow other modules to make changes
- // to the batch so that it is easier to reuse batch processes in other
- // environments.
- \Drupal::moduleHandler()->alter('batch', $batch);
-
- // Assign an arbitrary id: don't rely on a serial column in the 'batch'
- // table, since non-progressive batches skip database storage completely.
- $batch['id'] = db_next_id();
-
- // Move operations to a job queue. Non-progressive batches will use a
- // memory-based queue.
- foreach ($batch['sets'] as $key => $batch_set) {
- _batch_populate_queue($batch, $key);
- }
-
- // Initiate processing.
- if ($batch['progressive']) {
- // Now that we have a batch id, we can generate the redirection link in
- // the generic error message.
- /** @var \Drupal\Core\Url $batch_url */
- $batch_url = $batch['url'];
- /** @var \Drupal\Core\Url $error_url */
- $error_url = clone $batch_url;
- $query_options = $error_url->getOption('query');
- $query_options['id'] = $batch['id'];
- $query_options['op'] = 'finished';
- $error_url->setOption('query', $query_options);
-
- $batch['error_message'] = t('Please continue to the error page', [':error_url' => $error_url->toString(TRUE)->getGeneratedUrl()]);
-
- // Clear the way for the redirection to the batch processing page, by
- // saving and unsetting the 'destination', if there is any.
- $request = \Drupal::request();
- if ($request->query->has('destination')) {
- $batch['destination'] = $request->query->get('destination');
- $request->query->remove('destination');
- }
-
- // Store the batch.
- \Drupal::service('batch.storage')->create($batch);
-
- // Set the batch number in the session to guarantee that it will stay alive.
- $_SESSION['batches'][$batch['id']] = TRUE;
-
- // Redirect for processing.
- $query_options = $error_url->getOption('query');
- $query_options['op'] = 'start';
- $query_options['id'] = $batch['id'];
- $batch_url->setOption('query', $query_options);
- if (($function = $batch['redirect_callback']) && function_exists($function)) {
- $function($batch_url->toString(), ['query' => $query_options]);
- }
- else {
- return new RedirectResponse($batch_url->setAbsolute()->toString(TRUE)->getGeneratedUrl());
- }
- }
- else {
- // Non-progressive execution: bypass the whole progressbar workflow
- // and execute the batch in one pass.
- require_once __DIR__ . '/batch.inc';
- _batch_process();
- }
- }
+ return BatchQueueController::process($redirect, $url, $redirect_callback);
}
/**
@@ -887,69 +764,7 @@ function &batch_get() {
// that are part of the Batch API and need to reset the batch information may
// call batch_get() and manipulate the result by reference. Functions that are
// not part of the Batch API can also do this, but shouldn't.
- static $batch = [];
- return $batch;
-}
-
-/**
- * Populates a job queue with the operations of a batch set.
- *
- * Depending on whether the batch is progressive or not, the
- * Drupal\Core\Queue\Batch or Drupal\Core\Queue\BatchMemory handler classes will
- * be used. The name and class of the queue are added by reference to the
- * batch set.
- *
- * @param $batch
- * The batch array.
- * @param $set_id
- * The id of the set to process.
- */
-function _batch_populate_queue(&$batch, $set_id) {
- $batch_set = &$batch['sets'][$set_id];
-
- if (isset($batch_set['operations'])) {
- $batch_set += [
- 'queue' => [
- 'name' => 'drupal_batch:' . $batch['id'] . ':' . $set_id,
- 'class' => $batch['progressive'] ? 'Drupal\Core\Queue\Batch' : 'Drupal\Core\Queue\BatchMemory',
- ],
- ];
-
- $queue = _batch_queue($batch_set);
- $queue->createQueue();
- foreach ($batch_set['operations'] as $operation) {
- $queue->createItem($operation);
- }
-
- unset($batch_set['operations']);
- }
-}
-
-/**
- * Returns a queue object for a batch set.
- *
- * @param $batch_set
- * The batch set.
- *
- * @return
- * The queue object.
- */
-function _batch_queue($batch_set) {
- static $queues;
-
- if (!isset($queues)) {
- $queues = [];
- }
-
- if (isset($batch_set['queue'])) {
- $name = $batch_set['queue']['name'];
- $class = $batch_set['queue']['class'];
-
- if (!isset($queues[$class][$name])) {
- $queues[$class][$name] = new $class($name, \Drupal::database());
- }
- return $queues[$class][$name];
- }
+ return BatchQueueController::getBatches();
}
/**
diff --git a/core/lib/Drupal/Core/Batch/Batch.php b/core/lib/Drupal/Core/Batch/Batch.php
new file mode 100644
index 0000000..32a5ff0
--- /dev/null
+++ b/core/lib/Drupal/Core/Batch/Batch.php
@@ -0,0 +1,440 @@
+setTitle('Batch Title')
+ * ->setFinishCallback('batch_example_finished_callback')
+ * ->setInitMessage('The initialization message (optional)');
+ * for ($i = 0; $i < 1000; $i++) {
+ * $batch->addOperation('batch_example_callback', [$i + 1]);
+ * }
+ * $batch->enqueue();
+ * @endcode
+ */
+class Batch {
+
+ /**
+ * The set of operations to be processed.
+ *
+ * Each operation is a tuple of the function / method to use and an array
+ * containing any parameters to be passed.
+ *
+ * @var array
+ */
+ protected $operations = [];
+
+ /**
+ * The title for the batch.
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * The initializing message for the batch.
+ *
+ * @var string
+ */
+ protected $initMessage;
+
+ /**
+ * The message to be shown while the batch is in progress.
+ *
+ * @var string
+ */
+ protected $progressMessage;
+
+ /**
+ * The message to be shown if a problem occurs.
+ *
+ * @var string
+ */
+ protected $errorMessage;
+
+ /**
+ * The name of a function / method to be called when the batch finishes.
+ *
+ * @var string
+ */
+ protected $finished;
+
+ /**
+ * The file containing the operation and finished callbacks.
+ *
+ * If the callbacks are in the .module file or are static methods of a class,
+ * then this does not need to be set.
+ *
+ * @var string
+ */
+ protected $file;
+
+ /**
+ * An array of libraries to be included when processing the batch.
+ *
+ * @var string[]
+ */
+ protected $libraries = [];
+
+ /**
+ * An array of options to be used with the redirect URL.
+ *
+ * @var array
+ */
+ protected $urlOptions = [];
+
+ /**
+ * Specifies if the batch is progressive.
+ *
+ * If true, multiple calls are used. Otherwise an attempt is made to process
+ * the batch in on page call.
+ *
+ * @var bool
+ */
+ protected $progressive = TRUE;
+
+ /**
+ * The details of the queue to use.
+ *
+ * A tuple containing the name of the queue and the class of the queue to use.
+ *
+ * @var array
+ */
+ protected $queue;
+
+ /**
+ * Constructs a new Batch instance.
+ */
+ public function __construct() {
+ $this->setTitle('Processing');
+ $this->setInitMessage('Initializing.');
+ $this->setProgressMessage('Completed @current of @total.');
+ $this->setErrorMessage('An error has occurred.');
+ }
+
+ /**
+ * Static constructor for a batch process.
+ *
+ * @return static
+ * A new object.
+ */
+ public static function create() {
+ return new static();
+ }
+
+ /**
+ * Static constructor for a batch process with a title.
+ *
+ * @param string $title
+ * The title.
+ *
+ * @return static
+ * A new object.
+ */
+ public static function createFromTitle($title) {
+ return static::create()->setTitle($title);
+ }
+
+ /**
+ * Creates a \Drupal\Core\Batch\Batch object from an array.
+ *
+ * @param array $batch_definition
+ * An array of values to use for the new object.
+ *
+ * @return static
+ * A new initialized object.
+ */
+ public static function createFromArray(array $batch_definition) {
+ $new_batch = static::create();
+
+ if (isset($batch_definition['operations'])) {
+ foreach ($batch_definition['operations'] as list($callback, $arguments)) {
+ $new_batch->addOperation($callback, $arguments);
+ }
+ }
+
+ if (isset($batch_definition['title'])) {
+ $new_batch->setTitle($batch_definition['title']);
+ }
+
+ if (isset($batch_definition['init_message'])) {
+ $new_batch->setInitMessage($batch_definition['init_message']);
+ }
+
+ if (isset($batch_definition['progress_message'])) {
+ $new_batch->setProgressMessage($batch_definition['progress_message']);
+ }
+
+ if (isset($batch_definition['error_message'])) {
+ $new_batch->setErrorMessage($batch_definition['error_message']);
+ }
+
+ if (isset($batch_definition['finished'])) {
+ $new_batch->setFinishCallback($batch_definition['finished']);
+ }
+
+ if (isset($batch_definition['file'])) {
+ $new_batch->setFile($batch_definition['file']);
+ }
+
+ if (isset($batch_definition['library'])) {
+ $new_batch->addLibraries($batch_definition['library']);
+ }
+
+ if (isset($batch_definition['url_options'])) {
+ $new_batch->setUrlOptions($batch_definition['url_options']);
+ }
+
+ if (isset($batch_definition['progressive'])) {
+ $new_batch->setProgressive($batch_definition['progressive']);
+ }
+
+ if (
+ isset($batch_definition['queue']['name']) &&
+ isset($batch_definition['queue']['class'])
+ ) {
+ $new_batch->setQueue(
+ $batch_definition['queue']['name'],
+ $batch_definition['queue']['class']
+ );
+ }
+
+ return $new_batch;
+ }
+
+ /**
+ * Sets the title.
+ *
+ * @param string $title
+ * The title.
+ *
+ * @return $this
+ */
+ public function setTitle($title) {
+ $this->title = $title;
+ return $this;
+ }
+
+ /**
+ * Sets the finished callback.
+ *
+ * This callback will be executed if the batch process is done.
+ *
+ * @param string $callback
+ * The callback.
+ *
+ * @return $this
+ */
+ public function setFinishCallback($callback) {
+ $this->finished = $callback;
+ return $this;
+ }
+
+ /**
+ * Sets the displayed message while processing is initialized.
+ *
+ * @param string $message
+ * The text to display. Defaults to 'Initializing.'.
+ *
+ * @return $this
+ */
+ public function setInitMessage($message) {
+ $this->initMessage = $message;
+ return $this;
+ }
+
+ /**
+ * Sets the message to display when the batch is being processed.
+ *
+ * @param string $message
+ * The text to display. Available placeholders are '@current',
+ * '@remaining', '@total', '@percentage', '@estimate' and '@elapsed'.
+ * Defaults to 'Completed @current of @total.'.
+ *
+ * @return $this
+ */
+ public function setProgressMessage($message) {
+ $this->progressMessage = $message;
+ return $this;
+ }
+
+ /**
+ * Sets the message to display if an error occurs while processing.
+ *
+ * @param string $message
+ * The text to display. Defaults to 'An error has occurred.'.
+ *
+ * @return $this
+ */
+ public function setErrorMessage($message) {
+ $this->errorMessage = $message;
+ return $this;
+ }
+
+ /**
+ * Sets the file that contains the callback functions.
+ *
+ * The path should be relative to base_path(), and thus should be built using
+ * drupal_get_path(). Defaults to {module_name}.module.
+ *
+ * @param string $filename
+ * The path to the file.
+ *
+ * @return $this
+ */
+ public function setFile($filename) {
+ $this->file = $filename;
+ return $this;
+ }
+
+ /**
+ * Sets the libraries to use when processing the batch.
+ *
+ * Adds the libraries for use on the progress page. Any previously added
+ * libraries are removed. Use \Drupal\Core\Batch\Batch::addLibraries() to add
+ * one or more libraries.
+ *
+ * @param string[] $libraries
+ * The libraries to be used.
+ *
+ * @return $this
+ */
+ public function setLibraries(array $libraries) {
+ $this->libraries = $libraries;
+ return $this;
+ }
+
+ /**
+ * Sets the options for redirect URLs.
+ *
+ * @param array $options
+ * The options to use.
+ *
+ * @return $this
+ *
+ * @see \Drupal\Core\Url
+ */
+ public function setUrlOptions(array $options = []) {
+ $this->urlOptions = $options;
+ return $this;
+ }
+
+ /**
+ * Sets the batch to run progressively.
+ *
+ * @param bool $is_progressive
+ * TRUE (default) indicates that the batch will run in more than one run.
+ * FALSE indicates that the batch will finish in a single run.
+ *
+ * @return $this
+ */
+ public function setProgressive($is_progressive = TRUE) {
+ $this->progressive = $is_progressive;
+ return $this;
+ }
+
+ /**
+ * Sets or removes an override for the default queue.
+ *
+ * @param string $name
+ * The unique identifier for the queue.
+ * @param string $class
+ * The name of a class that implements \Drupal\Core\Queue\QueueInterface,
+ * including the full namespace but not starting with a backslash. It must
+ * have a constructor with two arguments: $name and a
+ * \Drupal\Core\Database\Connection object. Typically, the class will either
+ * be \Drupal\Core\Queue\Batch or \Drupal\Core\Queue\BatchMemory. Defaults
+ * to Batch if progressive is TRUE, or to BatchMemory if progressive is
+ * FALSE.
+ *
+ * @return $this
+ */
+ public function setQueue($name, $class) {
+ if ($name === NULL && $class === NULL) {
+ $this->queue = NULL;
+ return $this;
+ }
+
+ $this->queue = [
+ 'name' => $name,
+ 'class' => $class,
+ ];
+ return $this;
+ }
+
+ /**
+ * Adds a batch operation.
+ *
+ * @param string $callback
+ * The name of the callback function.
+ * @param array $arguments
+ * An array of arguments to pass to the callback function.
+ *
+ * @return $this
+ */
+ public function addOperation($callback, array $arguments = []) {
+ $this->operations[] = [$callback, $arguments];
+ return $this;
+ }
+
+ /**
+ * Adds libraries to be used on the process page.
+ *
+ * @param string|string[] $libraries
+ * The libraries to add.
+ *
+ * @return $this
+ */
+ public function addLibraries($libraries) {
+ if (!is_array($libraries)) {
+ $libraries = [$libraries];
+ }
+ $this->libraries = $this->libraries + $libraries;
+ return $this;
+ }
+
+ /**
+ * Places the batch in the queue to be processed.
+ *
+ * This is an alias for BatchQueueController::enqueue() to allow for
+ * pipe-lining of Batch objects.
+ *
+ * @return $this
+ *
+ * @see \Drupal\Core\Batch\BatchQueueController::enqueue()
+ */
+ public function enqueue() {
+ BatchQueueController::enqueue($this);
+ return $this;
+ }
+
+ /**
+ * Converts a \Drupal\Core\Batch\Batch object into an array.
+ *
+ * @return array
+ * The array representation of the object.
+ */
+ public function toArray() {
+ $array = [
+ 'operations' => $this->operations ?: [],
+ 'title' => $this->title ?: '',
+ 'init_message' => $this->initMessage ?: '',
+ 'progress_message' => $this->progressMessage ?: '',
+ 'error_message' => $this->errorMessage ?: '',
+ 'finished' => $this->finished,
+ 'file' => $this->file,
+ 'library' => $this->libraries ?: [],
+ 'url_options' => $this->urlOptions ?: [],
+ 'progressive' => $this->progressive,
+ 'queue' => $this->queue ?: NULL,
+ ];
+
+ return $array;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Batch/BatchQueueController.php b/core/lib/Drupal/Core/Batch/BatchQueueController.php
new file mode 100644
index 0000000..303c622
--- /dev/null
+++ b/core/lib/Drupal/Core/Batch/BatchQueueController.php
@@ -0,0 +1,607 @@
+enqueue() directly
+ * on the object.
+ *
+ * @param \Drupal\Core\Batch\Batch $batch
+ * The batch to place in the queue.
+ *
+ * @see \Drupal\Core\Batch\Batch::enqueue()
+ */
+ public static function enqueue(Batch $batch) {
+ // Initialize the batch if needed.
+ if (empty(self::$batches)) {
+ self::$batches = [
+ 'sets' => [],
+ 'has_form_submits' => FALSE,
+ ];
+ }
+
+ $init = [
+ 'sandbox' => [],
+ 'results' => [],
+ 'success' => FALSE,
+ 'start' => 0,
+ 'elapsed' => 0,
+ ];
+ $batch_set = $init + $batch->toArray();
+
+ // Make strings translatable.
+ if (is_string($batch_set['title'])) {
+ $batch_set['title'] = new TranslatableMarkup($batch_set['title']);
+ }
+ if (is_string($batch_set['init_message'])) {
+ $batch_set['init_message'] = new TranslatableMarkup($batch_set['init_message'] . '
');
+ }
+ elseif ($batch_set['init_message'] instanceof TranslatableMarkup) {
+ $batch_set['init_message'] = new TranslatableMarkup(
+ $batch_set['init_message']->getUntranslatedString() . '
',
+ $batch_set['init_message']->getArguments()
+ );
+ }
+ if (is_string($batch_set['progress_message'])) {
+ $batch_set['progress_message'] = new TranslatableMarkup($batch_set['progress_message']);
+ }
+ if (is_string($batch_set['error_message'])) {
+ $batch_set['error_message'] = new TranslatableMarkup($batch_set['error_message']);
+ }
+
+ // The non-concurrent workflow of batch execution allows us to save
+ // numberOfItems() queries by handling our own counter.
+ $batch_set['total'] = count($batch_set['operations']);
+ $batch_set['count'] = $batch_set['total'];
+ // Add the set to the batch.
+ if (empty(self::$batches['id'])) {
+ // The batch is not running yet. Simply add the new set.
+ self::$batches['sets'][] = $batch_set;
+ }
+ else {
+ // The set is being added while the batch is running. Insert the new set
+ // right after the current one to ensure execution order, and store its
+ // operations in a queue.
+ $index = self::$batches['current_set'] + 1;
+ $slice1 = array_slice(self::$batches['sets'], 0, $index);
+ $slice2 = array_slice(self::$batches['sets'], $index);
+ self::$batches['sets'] = array_merge($slice1, [$batch_set], $slice2);
+ self::populateQueue($index);
+ }
+ }
+
+ /**
+ * Processes the batch.
+ *
+ * This function is generally not needed in form submit handlers;
+ * Form API takes care of batches that were set during form submission.
+ *
+ * @param \Drupal\Core\Url|string $redirect
+ * (optional) Either path or Url object to redirect to when the batch has
+ * finished processing. Note that to simply force a batch to (conditionally)
+ * redirect to a custom location after it is finished processing but to
+ * otherwise allow the standard form API batch handling to occur, it is not
+ * necessary to call batch_process() and use this parameter. Instead, make
+ * the batch 'finished' callback return an instance of
+ * \Symfony\Component\HttpFoundation\RedirectResponse, which will be used
+ * automatically by the standard batch processing pipeline (and which takes
+ * precedence over this parameter).
+ * @param \Drupal\Core\Url $url
+ * (optional) URL of the batch processing page.
+ * Should only be used for separate scripts like update.php.
+ * @param string $redirect_callback
+ * (optional) Specify a function to be called to redirect to the progressive
+ * processing page.
+ *
+ * @return \Symfony\Component\HttpFoundation\RedirectResponse|null
+ * A redirect response if the batch is progressive. No return value
+ * otherwise.
+ */
+ public static function process($redirect = NULL, Url $url = NULL, $redirect_callback = NULL) {
+ if (isset(self::$batches)) {
+ // Add process information.
+ $process_info = [
+ 'current_set' => 0,
+ 'progressive' => TRUE,
+ 'url' => isset($url) ? $url : Url::fromRoute('system.batch_page.html'),
+ 'source_url' => Url::fromRouteMatch(\Drupal::routeMatch())->mergeOptions(['query' => \Drupal::request()->query->all()]),
+ 'batch_redirect' => $redirect,
+ 'theme' => \Drupal::theme()->getActiveTheme()->getName(),
+ 'redirect_callback' => $redirect_callback,
+ ];
+ self::$batches += $process_info;
+
+ // The batch is now completely built. Allow other modules to make changes
+ // to the batch so that it is easier to reuse batch processes in other
+ // environments.
+ \Drupal::moduleHandler()->alter('batch', self::$batches);
+
+ // Assign an arbitrary id: don't rely on a serial column in the 'batch'
+ // table, since non-progressive batches skip database storage completely.
+ self::$batches['id'] = Database::getConnection()->nextId();
+
+ // Move operations to a job queue. Non-progressive batches will use a
+ // memory-based queue.
+ foreach (self::$batches['sets'] as $key => $batch_set) {
+ self::populateQueue($key);
+ }
+
+ // Initiate processing.
+ if (self::$batches['progressive']) {
+ // Now that we have a batch id, we can generate the redirection link in
+ // the generic error message.
+ /** @var \Drupal\Core\Url $batch_url */
+ $batch_url = self::$batches['url'];
+ /** @var \Drupal\Core\Url $error_url */
+ $error_url = clone $batch_url;
+ $query_options = $error_url->getOption('query');
+ $query_options['id'] = self::$batches['id'];
+ $query_options['op'] = 'finished';
+ $error_url->setOption('query', $query_options);
+
+ self::$batches['error_message'] = t('Please continue to the error page', [':error_url' => $error_url->toString(TRUE)->getGeneratedUrl()]);
+
+ // Clear the way for the redirection to the batch processing page, by
+ // saving and un-setting the 'destination', if there is any.
+ $request = \Drupal::request();
+ if ($request->query->has('destination')) {
+ self::$batches['destination'] = $request->query->get('destination');
+ $request->query->remove('destination');
+ }
+
+ // Store the batch.
+ \Drupal::service('batch.storage')->create(self::$batches);
+
+ // Set the batch number in the session to guarantee that it will stay
+ // alive.
+ $_SESSION['batches'][self::$batches['id']] = TRUE;
+
+ // Redirect for processing.
+ $query_options = $error_url->getOption('query');
+ $query_options['op'] = 'start';
+ $query_options['id'] = self::$batches['id'];
+ $batch_url->setOption('query', $query_options);
+ if (($function = self::$batches['redirect_callback']) && function_exists($function)) {
+ $function($batch_url->toString(), ['query' => $query_options]);
+ }
+ else {
+ return new RedirectResponse($batch_url->setAbsolute()->toString(TRUE)->getGeneratedUrl());
+ }
+ }
+ else {
+ // Non-progressive execution: bypass the whole progressbar workflow
+ // and execute the batch in one pass.
+ self::processQueue();
+ }
+ }
+
+ return NULL;
+ }
+
+ /**
+ * Retrieves the current batch.
+ */
+ public static function &getBatches() {
+ return self::$batches;
+ }
+
+ /**
+ * Returns a queue object for a batch set.
+ *
+ * @param array $batch
+ * The batch set.
+ *
+ * @return \Drupal\Core\Queue\QueueInterface|null
+ * The queue object or null if the queue cannot be created.
+ */
+ public static function getQueueForBatch(array $batch) {
+ if (isset($batch['queue'])) {
+ return self::getQueue($batch['queue']['name'], $batch['queue']['class']);
+ }
+ return NULL;
+ }
+
+ /**
+ * Returns the batch set being currently processed.
+ *
+ * @return array
+ * The current batch set.
+ *
+ * @internal
+ */
+ public static function &getCurrentSet() {
+ return self::$batches['sets'][self::$batches['current_set']];
+ }
+
+ /**
+ * Retrieves the next set in a batch.
+ *
+ * If there is a subsequent set in this batch, assign it as the new set to
+ * process and execute its form submit handler (if defined), which may add
+ * further sets to this batch.
+ *
+ * @return bool
+ * TRUE if a subsequent set was found in the batch; FALSE will be returned
+ * if no subsequent set was found.
+ *
+ * @internal
+ */
+ public static function nextSet() {
+ if (isset(self::$batches['sets'][self::$batches['current_set'] + 1])) {
+ self::$batches['current_set']++;
+ $current_set = &self::getCurrentSet();
+ if (isset($current_set['form_submit']) && ($callback = $current_set['form_submit']) && is_callable($callback)) {
+ // We use our stored copies of $form and $form_state to account for
+ // possible alterations by previous form submit handlers.
+ $complete_form = &self::$batches['form_state']->getCompleteForm();
+ call_user_func_array($callback, [$complete_form, self::$batches['form_state']]);
+ }
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Processes sets in a batch.
+ *
+ * If the batch was marked for progressive execution (default), this executes
+ * as many operations in batch sets until an execution time of 1 second has
+ * been exceeded. It will continue with the next operation of the same batch
+ * set in the next request.
+ *
+ * @return \Symfony\Component\HttpFoundation\RedirectResponse|array|false
+ * An array containing a completion value (in percent) and a status message.
+ *
+ * @internal
+ */
+ public static function processQueue() {
+ $current_set = &self::getCurrentSet();
+ // Indicate that this batch set needs to be initialized.
+ $set_changed = TRUE;
+
+ // If this batch was marked for progressive execution (e.g. forms submitted
+ // by \Drupal::formBuilder()->submitForm(), initialize a timer to determine
+ // whether we need to proceed with the same batch phase when a processing
+ // time of 1 second has been exceeded.
+ if (self::$batches['progressive']) {
+ Timer::start('batch_processing');
+ }
+
+ if (empty($current_set['start'])) {
+ $current_set['start'] = microtime(TRUE);
+ }
+
+ $queue = self::getQueueForBatch($current_set);
+ $finished = 0;
+ $label = '';
+ $old_set = $current_set;
+
+ while (!$current_set['success']) {
+ // If this is the first time we iterate this batch set in the current
+ // request, we check if it requires an additional file for functions
+ // definitions.
+ if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) {
+ include_once \Drupal::root() . '/' . $current_set['file'];
+ }
+
+ $task_message = $label = '';
+ // Assume a single pass operation and set the completion level to 1 by
+ // default.
+ $finished = 1;
+
+ if ($item = $queue->claimItem()) {
+ list($callback, $args) = $item->data;
+
+ // Build the 'context' array and execute the function call.
+ $batch_context = [
+ 'sandbox' => &$current_set['sandbox'],
+ 'results' => &$current_set['results'],
+ 'finished' => &$finished,
+ 'message' => &$task_message,
+ ];
+ call_user_func_array($callback, array_merge($args, [&$batch_context]));
+
+ if ($finished >= 1) {
+ // Make sure this step is not counted twice when computing $current.
+ $finished = 0;
+ // Remove the processed operation and clear the sandbox.
+ $queue->deleteItem($item);
+ $current_set['count']--;
+ $current_set['sandbox'] = [];
+ }
+ }
+
+ // When all operations in the current batch set are completed, browse
+ // through the remaining sets, marking them 'successfully processed'
+ // along the way, until we find a set that contains operations.
+ // _batch_next_set() executes form submit handlers stored in 'control'
+ // sets (see \Drupal::service('form_submitter')), which can in turn add
+ // new sets to the batch.
+ $set_changed = FALSE;
+ $old_set = $current_set;
+ while (empty($current_set['count']) && ($current_set['success'] = TRUE) && self::nextSet()) {
+ $current_set = &self::getCurrentSet();
+ $current_set['start'] = microtime(TRUE);
+ $set_changed = TRUE;
+ }
+
+ // At this point, either $current_set contains operations that need to be
+ // processed or all sets have been completed.
+ $queue = self::getQueueForBatch($current_set);
+
+ // If we are in progressive mode, break processing after 1 second.
+ if (self::$batches['progressive'] && Timer::read('batch_processing') > 1000) {
+ // Record elapsed wall clock time.
+ $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
+ break;
+ }
+ }
+
+ if (self::$batches['progressive']) {
+ // Gather progress information.
+ //
+ // Reporting 100% progress will cause the whole batch to be considered
+ // processed. If processing was paused right after moving to a new set,
+ // we have to use the info from the new (unprocessed) set.
+ if ($set_changed && isset($current_set['queue'])) {
+ // Processing will continue with a fresh batch set.
+ $remaining = $current_set['count'];
+ $total = $current_set['total'];
+ $progress_message = $current_set['init_message'];
+ $task_message = '';
+ }
+ else {
+ // Processing will continue with the current batch set.
+ $remaining = $old_set['count'];
+ $total = $old_set['total'];
+ $progress_message = $old_set['progress_message'];
+ }
+
+ // Total progress is the number of operations that have fully run plus the
+ // completion level of the current operation.
+ $current = $total - $remaining + $finished;
+ $percentage = Percentage::format($total, $current);
+ $elapsed = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0;
+ $values = [
+ '@remaining' => $remaining,
+ '@total' => $total,
+ '@current' => floor($current),
+ '@percentage' => $percentage,
+ '@elapsed' => \Drupal::service('date.formatter')->formatInterval($elapsed / 1000),
+ // If possible, estimate remaining processing time.
+ '@estimate' => ($current > 0) ? \Drupal::service('date.formatter')->formatInterval(($elapsed * ($total - $current) / $current) / 1000) : '-',
+ ];
+ $message = strtr($progress_message, $values);
+ if (!empty($task_message)) {
+ $label = $task_message;
+ }
+
+ return [$percentage, $message, $label];
+ }
+ else {
+ // If we are not in progressive mode, the entire batch has been processed.
+ return self::finishedProcessing();
+ }
+ }
+
+ /**
+ * Ends the batch processing.
+ *
+ * Call the 'finished' callback of each batch set to allow custom handling of
+ * the results and resolve page redirection.
+ *
+ * @return \Symfony\Component\HttpFoundation\RedirectResponse|false
+ * A redirect response to the completed page or NULL to stay at the current
+ * URL.
+ *
+ * @internal
+ */
+ public static function finishedProcessing() {
+ self::$batches = &batch_get();
+ $batch_finished_redirect = NULL;
+
+ // Execute the 'finished' callbacks for each batch set, if defined.
+ foreach (self::$batches['sets'] as $batch_set) {
+ if (isset($batch_set['finished'])) {
+ // Check if the set requires an additional file for function
+ // definitions.
+ if (isset($batch_set['file']) && is_file($batch_set['file'])) {
+ include_once \Drupal::root() . '/' . $batch_set['file'];
+ }
+ if (is_callable($batch_set['finished'])) {
+ $queue = self::getQueueForBatch($batch_set);
+ $operations = $queue->getAllItems();
+ $batch_set_result = call_user_func_array(
+ $batch_set['finished'],
+ [
+ $batch_set['success'],
+ $batch_set['results'],
+ $operations,
+ \Drupal::service('date.formatter')
+ ->formatInterval($batch_set['elapsed'] / 1000),
+ ]
+ );
+ // If a batch 'finished' callback requested a redirect after the batch
+ // is complete, save that for later use. If more than one batch set
+ // returned a redirect, the last one is used.
+ if ($batch_set_result instanceof RedirectResponse) {
+ $batch_finished_redirect = $batch_set_result;
+ }
+ }
+ }
+ }
+
+ // Clean up the batch table and unset the static $batch variable.
+ if (self::$batches['progressive']) {
+ \Drupal::service('batch.storage')->delete(self::$batches['id']);
+ foreach (self::$batches['sets'] as $batch_set) {
+ if ($queue = self::getQueueForBatch($batch_set)) {
+ $queue->deleteQueue();
+ }
+ }
+ // Clean-up the session. Not needed for CLI updates.
+ if (isset($_SESSION)) {
+ unset($_SESSION['batches'][self::$batches['id']]);
+ if (empty($_SESSION['batches'])) {
+ unset($_SESSION['batches']);
+ }
+ }
+ }
+ $_batch = self::$batches;
+ self::$batches = NULL;
+
+ // Redirect if needed.
+ if ($_batch['progressive']) {
+ // Revert the 'destination' that was saved in batch_process().
+ if (isset($_batch['destination'])) {
+ \Drupal::request()->query->set('destination', $_batch['destination']);
+ }
+
+ // Determine the target path to redirect to. If a batch 'finished'
+ // callback returned a redirect response object, use that. Otherwise, fall
+ // back on the form redirection.
+ if (isset($batch_finished_redirect)) {
+ return $batch_finished_redirect;
+ }
+ elseif (!isset($_batch['form_state'])) {
+ $_batch['form_state'] = new FormState();
+ }
+ if ($_batch['form_state']->getRedirect() === NULL) {
+ $redirect = $_batch['batch_redirect'] ?: $_batch['source_url'];
+ // Any path with a scheme does not correspond to a route.
+ if (!$redirect instanceof Url) {
+ $options = UrlHelper::parse($redirect);
+ if (parse_url($options['path'], PHP_URL_SCHEME)) {
+ $redirect = Url::fromUri($options['path'], $options);
+ }
+ else {
+ $redirect = \Drupal::pathValidator()->getUrlIfValid($options['path']);
+ if (!$redirect) {
+ // Stay on the same page if the redirect was invalid.
+ $redirect = Url::fromRoute('');
+ }
+ $redirect->setOptions($options);
+ }
+ }
+ $_batch['form_state']->setRedirectUrl($redirect);
+ }
+
+ // Use \Drupal\Core\Form\FormSubmitterInterface::redirectForm() to handle
+ // the redirection logic.
+ $redirect = \Drupal::service('form_submitter')->redirectForm($_batch['form_state']);
+ if (is_object($redirect)) {
+ return $redirect;
+ }
+
+ // If no redirection happened, redirect to the originating page. In case
+ // the form needs to be rebuilt, save the final $form_state for
+ // \Drupal\Core\Form\FormBuilderInterface::buildForm().
+ if ($_batch['form_state']->isRebuilding()) {
+ $_SESSION['batch_form_state'] = $_batch['form_state'];
+ }
+ $callback = $_batch['redirect_callback'];
+ /** @var \Drupal\Core\Url $source_url */
+ $_batch['source_url']->mergeOptions(['query' => ['op' => 'finish', 'id' => $_batch['id']]]);
+ if (is_callable($callback)) {
+ $callback($_batch['source_url'], $_batch['source_url']->getOption('query'));
+ }
+ elseif ($callback === NULL) {
+ // Default to RedirectResponse objects when nothing specified.
+ return new RedirectResponse($_batch['source_url']->setAbsolute()->toString());
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Returns a queue object with the given name and class.
+ *
+ * @param string $name
+ * The name of the queue.
+ * @param string $class
+ * The class of the queue.
+ * Usually \Drupal\Core\Queue\Batch or \Drupal\Core\Queue\BatchMemory.
+ *
+ * @return \Drupal\Core\Queue\QueueInterface
+ * The queue object.
+ */
+ protected static function getQueue($name, $class) {
+ if (!isset(self::$queues[$class][$name])) {
+ self::$queues[$class][$name] = new $class($name, \Drupal::database());
+ }
+ return self::$queues[$class][$name];
+ }
+
+ /**
+ * Populates a job queue with the operations of a batch set.
+ *
+ * Depending on whether the batch is progressive or not, the
+ * Drupal\Core\Queue\Batch or Drupal\Core\Queue\BatchMemory handler classes
+ * will be used. The name and class of the queue are added by reference to the
+ * batch set.
+ *
+ * @param int $set_id
+ * The id of the set to process.
+ */
+ protected static function populateQueue($set_id = 0) {
+ $batch = &self::$batches;
+ $batch_set = &$batch['sets'][$set_id];
+
+ if (isset($batch_set['operations'])) {
+ if (empty($batch_set['queue'])) {
+ $batch_set['queue'] = [
+ 'name' => 'drupal_batch:' . $batch['id'] . ':' . $set_id,
+ 'class' => (
+ $batch['progressive'] ? BatchQueue::class : BatchMemory::class
+ ),
+ ];
+ }
+
+ $queue = self::getQueueForBatch($batch_set);
+ $queue->createQueue();
+ foreach ($batch_set['operations'] as $operation) {
+ $queue->createItem($operation);
+ }
+
+ unset($batch_set['operations']);
+ }
+ }
+
+}