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']); + } + } + +}