diff --git a/core/includes/batch.inc b/core/includes/batch.inc index 23599e1279bbb7645b56ba15e17c458495a6da4c..af7987fd8fe02da6d45e775a240e7822c222e67b 100644 --- a/core/includes/batch.inc +++ b/core/includes/batch.inc @@ -418,8 +418,10 @@ function &_batch_current_set() { */ function _batch_next_set() { $batch = &batch_get(); - if (isset($batch['sets'][$batch['current_set'] + 1])) { - $batch['current_set']++; + $set_indexes = array_keys($batch['sets']); + $current_set_index_key = array_search($batch['current_set'], $set_indexes); + if (isset($set_indexes[$current_set_index_key + 1])) { + $batch['current_set'] = $set_indexes[$current_set_index_key + 1]; $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 diff --git a/core/includes/form.inc b/core/includes/form.inc index 02a97c4c1303e4b65765a07f5c02e2c36b5ab36a..fe3230707e428cc4e22d76fec150629600512aa9 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -776,18 +776,70 @@ function batch_set($batch_definition) { $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); + // The set is being added while the batch is running. + _batch_append_set($batch, $batch_set); } } } +/** + * Appends a batch set to a running batch. + * + * Inserts the new set right after the current one to ensure execution order, + * and stores its operations in a queue. If the current batch has already + * inserted a new set, additional sets will be inserted after the last inserted + * set. + * + * @param &$batch + * The batch array. + * @param $batch_set + * The batch set. + */ +function _batch_append_set(&$batch, $batch_set) { + $append_after_index = $batch['current_set']; + $reached_current_set = FALSE; + foreach ($batch['sets'] as $index => $set) { + // As the indexes are not ordered numerically we need to first reach the + // index of the current set and then search for the proper place to append + // the new batch set. + if (!$reached_current_set) { + if ($index == $batch['current_set']) { + $reached_current_set = TRUE; + } + continue; + } + if ($index > $append_after_index) { + if (isset($set['appended_after_index'])) { + $append_after_index = $index; + } + else { + break; + } + } + } + $batch_set['appended_after_index'] = $append_after_index; + + // Iterate by reference over the existing batch sets and assign them by + // reference in the new batch sets array in order not to break a retrieved + // reference to the current set. Among other places a reference to the current + // set is being retrieved in _batch_process(). Additionally, we have to + // preserve the original indexes, as they are used to generate the queue name + // of each batch set, otherwise the operations of the new batch set will be + // queued in the queue of a previous batch set. + // @see _batch_populate_queue(). + $new_sets = []; + foreach ($batch['sets'] as $index => &$set) { + $new_sets[$index] = &$set; + if ($index == $append_after_index) { + $new_set_index = count($batch['sets']); + $new_sets[$new_set_index] = $batch_set; + } + } + + $batch['sets'] = $new_sets; + _batch_populate_queue($batch, $new_set_index); +} + /** * Processes the batch. * diff --git a/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc b/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc index eb1832185e8327f7b1f35888898efb605f187f97..35c18fec75ca5a5de8194062020ad9b16286ef71 100644 --- a/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc +++ b/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc @@ -15,7 +15,7 @@ * Performs a simple batch operation. */ function _batch_test_callback_1($id, $sleep, &$context) { - // No-op, but ensure the batch take a couple iterations. + // No-op, but ensure the batch takes a couple iterations. // Batch needs time to run for the test, so sleep a bit. usleep($sleep); // Track execution, and store some result for post-processing in the @@ -39,7 +39,7 @@ function _batch_test_callback_2($start, $total, $sleep, &$context) { // Process by groups of 5 (arbitrary value). $limit = 5; for ($i = 0; $i < $limit && $context['sandbox']['count'] < $total; $i++) { - // No-op, but ensure the batch take a couple iterations. + // No-op, but ensure the batch takes a couple iterations. // Batch needs time to run for the test, so sleep a bit. usleep($sleep); // Track execution, and store some result for post-processing in the @@ -63,7 +63,7 @@ function _batch_test_callback_2($start, $total, $sleep, &$context) { * Implements callback_batch_operation(). */ function _batch_test_callback_5($id, $sleep, &$context) { - // No-op, but ensure the batch take a couple iterations. + // No-op, but ensure the batch takes a couple iterations. // Batch needs time to run for the test, so sleep a bit. usleep($sleep); // Track execution, and store some result for post-processing in the @@ -77,15 +77,50 @@ function _batch_test_callback_5($id, $sleep, &$context) { /** * Implements callback_batch_operation(). * - * Performs a batch operation setting up its own batch. + * Performs a simple batch operation. + */ +function _batch_test_callback_6($id, $sleep, &$context) { + // No-op, but ensure the batch takes a couple iterations. + // Batch needs time to run for the test, so sleep a bit. + usleep($sleep); + // Track execution, and store some result for post-processing in the + // 'finished' callback. + batch_test_stack("op 6 id $id"); + $context['results'][6][] = $id; +} + +/** + * Implements callback_batch_operation(). + * + * Performs a simple batch operation. */ -function _batch_test_nested_batch_callback() { - batch_test_stack('setting up batch 2'); - batch_set(_batch_test_batch_2()); +function _batch_test_callback_7($id, $sleep, &$context) { + // No-op, but ensure the batch takes a couple iterations. + // Batch needs time to run for the test, so sleep a bit. + usleep($sleep); + // Track execution, and store some result for post-processing in the + // 'finished' callback. + batch_test_stack("op 7 id $id"); + $context['results'][7][] = $id; } /** - * Provides a common 'finished' callback for batches 1 to 4. + * Implements callback_batch_operation(). + * + * Performs a batch operation setting up its own batch(es). + */ +function _batch_test_nested_batch_callback(array $batches = []) { + foreach ($batches as $batch) { + batch_test_stack("setting up batch $batch"); + $function = '_batch_test_batch_' . $batch; + batch_set($function()); + } + \Drupal::state() + ->set('batch_test_nested_order_multiple_batches', batch_get()); +} + +/** + * Provides a common 'finished' callback for batches 1 to 7. */ function _batch_test_finished_helper($batch_id, $success, $results, $operations) { if ($results) { @@ -182,3 +217,21 @@ function _batch_test_finished_4($success, $results, $operations) { function _batch_test_finished_5($success, $results, $operations) { _batch_test_finished_helper(5, $success, $results, $operations); } + +/** + * Implements callback_batch_finished(). + * + * Triggers 'finished' callback for batch 6. + */ +function _batch_test_finished_6($success, $results, $operations) { + _batch_test_finished_helper(6, $success, $results, $operations); +} + +/** + * Implements callback_batch_finished(). + * + * Triggers 'finished' callback for batch 7. + */ +function _batch_test_finished_7($success, $results, $operations) { + _batch_test_finished_helper(7, $success, $results, $operations); +} diff --git a/core/modules/system/tests/modules/batch_test/batch_test.module b/core/modules/system/tests/modules/batch_test/batch_test.module index 59327de4fe2cd9998c89e9da34b8feffe2f40329..03d1e8e287515454b8d24d20bae505177f203c8a 100644 --- a/core/modules/system/tests/modules/batch_test/batch_test.module +++ b/core/modules/system/tests/modules/batch_test/batch_test.module @@ -24,6 +24,7 @@ function _batch_test_batch_0() { 'operations' => [], 'finished' => '_batch_test_finished_0', 'file' => drupal_get_path('module', 'batch_test') . '/batch_test.callbacks.inc', + 'batch_test_id' => 'batch_0', ]; return $batch; } @@ -46,6 +47,7 @@ function _batch_test_batch_1() { 'operations' => $operations, 'finished' => '_batch_test_finished_1', 'file' => drupal_get_path('module', 'batch_test') . '/batch_test.callbacks.inc', + 'batch_test_id' => 'batch_1', ]; return $batch; } @@ -67,6 +69,7 @@ function _batch_test_batch_2() { 'operations' => $operations, 'finished' => '_batch_test_finished_2', 'file' => drupal_get_path('module', 'batch_test') . '/batch_test.callbacks.inc', + 'batch_test_id' => 'batch_2', ]; return $batch; } @@ -98,6 +101,7 @@ function _batch_test_batch_3() { 'operations' => $operations, 'finished' => '_batch_test_finished_3', 'file' => drupal_get_path('module', 'batch_test') . '/batch_test.callbacks.inc', + 'batch_test_id' => 'batch_3', ]; return $batch; } @@ -119,7 +123,7 @@ function _batch_test_batch_4() { for ($i = 1; $i <= round($total / 2); $i++) { $operations[] = ['_batch_test_callback_1', [$i, $sleep]]; } - $operations[] = ['_batch_test_nested_batch_callback', []]; + $operations[] = ['_batch_test_nested_batch_callback', [[2]]]; for ($i = round($total / 2) + 1; $i <= $total; $i++) { $operations[] = ['_batch_test_callback_1', [$i, $sleep]]; } @@ -127,6 +131,7 @@ function _batch_test_batch_4() { 'operations' => $operations, 'finished' => '_batch_test_finished_4', 'file' => drupal_get_path('module', 'batch_test') . '/batch_test.callbacks.inc', + 'batch_test_id' => 'batch_4', ]; return $batch; } @@ -149,6 +154,61 @@ function _batch_test_batch_5() { 'operations' => $operations, 'finished' => '_batch_test_finished_5', 'file' => drupal_get_path('module', 'batch_test') . '/batch_test.callbacks.inc', + 'batch_test_id' => 'batch_5', + ]; + return $batch; +} + +/** + * Batch 6: Repeats a simple operation. + * + * Operations: op 6 from 1 to 10. + */ +function _batch_test_batch_6() { + // Ensure the batch takes at least two iterations. + $total = 10; + $sleep = (1000000 / $total) * 2; + + $operations = []; + for ($i = 1; $i <= $total; $i++) { + $operations[] = ['_batch_test_callback_6', [$i, $sleep]]; + } + $batch = [ + 'operations' => $operations, + 'finished' => '_batch_test_finished_6', + 'file' => drupal_get_path('module', 'batch_test') . '/batch_test.callbacks.inc', + 'batch_test_id' => 'batch_6', + ]; + return $batch; +} + +/** + * Batch 7: Performs two batches within a batch. + * + * Operations: + * - op 7 from 1 to 5, + * - set batch 5 (op 5 from 1 to 10, should run at the end before batch 2) + * - set batch 6 (op 6 from 1 to 10, should run at the end after batch 1) + * - op 7 from 6 to 10, + */ +function _batch_test_batch_7() { + // Ensure the batch takes at least two iterations. + $total = 10; + $sleep = (1000000 / $total) * 2; + + $operations = []; + for ($i = 1; $i <= $total / 2; $i++) { + $operations[] = ['_batch_test_callback_7', [$i, $sleep]]; + } + $operations[] = ['_batch_test_nested_batch_callback', [[6, 5]]]; + for ($i = ($total / 2) + 1; $i <= $total; $i++) { + $operations[] = ['_batch_test_callback_7', [$i, $sleep]]; + } + $batch = [ + 'operations' => $operations, + 'finished' => '_batch_test_finished_7', + 'file' => drupal_get_path('module', 'batch_test') . '/batch_test.callbacks.inc', + 'batch_test_id' => 'batch_7', ]; return $batch; } @@ -187,13 +247,14 @@ function _batch_test_title_callback() { * Helper function: Stores or retrieves traced execution data. */ function batch_test_stack($data = NULL, $reset = FALSE) { + $state = \Drupal::state(); if ($reset) { - \Drupal::state()->delete('batch_test.stack'); + $state->delete('batch_test.stack'); } if (!isset($data)) { - return \Drupal::state()->get('batch_test.stack'); + return $state->get('batch_test.stack'); } - $stack = \Drupal::state()->get('batch_test.stack'); + $stack = $state->get('batch_test.stack'); $stack[] = $data; - \Drupal::state()->set('batch_test.stack', $stack); + $state->set('batch_test.stack', $stack); } diff --git a/core/modules/system/tests/modules/batch_test/src/Form/BatchTestSimpleForm.php b/core/modules/system/tests/modules/batch_test/src/Form/BatchTestSimpleForm.php index f00d610d165683efe9936da9b1b7c5b34a0baa72..0cf0b301d6085226d1601f40d092211d43a26627 100644 --- a/core/modules/system/tests/modules/batch_test/src/Form/BatchTestSimpleForm.php +++ b/core/modules/system/tests/modules/batch_test/src/Form/BatchTestSimpleForm.php @@ -32,7 +32,10 @@ public function buildForm(array $form, FormStateInterface $form_state) { 'batch_2' => 'batch 2', 'batch_3' => 'batch 3', 'batch_4' => 'batch 4', + 'batch_6' => 'batch 6', + 'batch_7' => 'batch 7', ], + '#multiple' => TRUE, ]; $form['submit'] = [ '#type' => 'submit', @@ -48,8 +51,10 @@ public function buildForm(array $form, FormStateInterface $form_state) { public function submitForm(array &$form, FormStateInterface $form_state) { batch_test_stack(NULL, TRUE); - $function = '_batch_test_' . $form_state->getValue('batch'); - batch_set($function()); + foreach ($form_state->getValue('batch') as $batch) { + $function = '_batch_test_' . $batch; + batch_set($function()); + } $form_state->setRedirect('batch_test.redirect'); } diff --git a/core/modules/system/tests/src/Functional/Batch/ProcessingTest.php b/core/modules/system/tests/src/Functional/Batch/ProcessingTest.php index c48ff22383d8ada9ae94aaca0ffe320da4c9fa9c..6d73c1a8be87319f37bf5d5902e73ffa06bb5f72 100644 --- a/core/modules/system/tests/src/Functional/Batch/ProcessingTest.php +++ b/core/modules/system/tests/src/Functional/Batch/ProcessingTest.php @@ -89,6 +89,31 @@ public function testBatchForm() { $this->assertBatchMessages($this->_resultMessages('batch_4'), 'Nested batch performed successfully.'); $this->assertEqual(batch_test_stack(), $this->_resultStack('batch_4'), 'Execution order was correct.'); $this->assertText('Redirection successful.', 'Redirection after batch execution is correct.'); + + // Submit batches 4 and 7. Batch 4 will trigger batch 2. Batch 7 will + // trigger batches 6 and 5. + $edit = ['batch' => ['batch_4', 'batch_7']]; + $this->drupalPostForm('batch-test', $edit, 'Submit'); + $this->assertSession()->assertNoEscaped('<'); + $this->assertSession()->responseContains('Redirection successful.'); + $this->assertBatchMessages($this->_resultMessages('batch_4'), 'Nested batch performed successfully.'); + $this->assertBatchMessages($this->_resultMessages('batch_7'), 'Nested batch performed successfully.'); + $expected_stack = array_merge($this->_resultStack('batch_4'), $this->_resultStack('batch_7')); + $this->assertEquals($expected_stack, batch_test_stack(), 'Execution order was correct.'); + $batch = \Drupal::state()->get('batch_test_nested_order_multiple_batches'); + $this->assertEquals(5, count($batch['sets'])); + // Ensure correct queue mapping. + foreach ($batch['sets'] as $index => $batch_set) { + $this->assertEquals('drupal_batch:' . $batch['id'] . ':' . $index, $batch_set['queue']['name']); + } + // Ensure correct order of the nested batches. We reset the indexes in + // order to directly access the batches by their order. + $batch_sets = array_values($batch['sets']); + $this->assertEquals('batch_4', $batch_sets[0]['batch_test_id']); + $this->assertEquals('batch_2', $batch_sets[1]['batch_test_id']); + $this->assertEquals('batch_7', $batch_sets[2]['batch_test_id']); + $this->assertEquals('batch_6', $batch_sets[3]['batch_test_id']); + $this->assertEquals('batch_5', $batch_sets[4]['batch_test_id']); } /** @@ -246,6 +271,25 @@ public function _resultStack($id, $value = 0) { } break; + case 'batch_6': + for ($i = 1; $i <= 10; $i++) { + $stack[] = "op 6 id $i"; + } + break; + + case 'batch_7': + for ($i = 1; $i <= 5; $i++) { + $stack[] = "op 7 id $i"; + } + $stack[] = 'setting up batch 6'; + $stack[] = 'setting up batch 5'; + for ($i = 6; $i <= 10; $i++) { + $stack[] = "op 7 id $i"; + } + $stack = array_merge($stack, $this->_resultStack('batch_6')); + $stack = array_merge($stack, $this->_resultStack('batch_5')); + break; + case 'chained': $stack[] = 'submit handler 1'; $stack[] = 'value = ' . $value; @@ -295,6 +339,16 @@ public function _resultMessages($id) { $messages[] = 'results for batch 5<div class="item-list"><ul><li>op 5: processed 10 elements</li></ul></div>'; break; + case 'batch_6': + $messages[] = 'results for batch 6<div class="item-list"><ul><li>op 6: processed 10 elements</li></ul></div>'; + break; + + case 'batch_7': + $messages[] = 'results for batch 7<div class="item-list"><ul><li>op 7: processed 10 elements</li></ul></div>'; + $messages = array_merge($messages, $this->_resultMessages('batch_6')); + $messages = array_merge($messages, $this->_resultMessages('batch_5')); + break; + case 'chained': $messages = array_merge($messages, $this->_resultMessages('batch_1')); $messages = array_merge($messages, $this->_resultMessages('batch_2'));