diff --git a/core/lib/Drupal/Core/Batch/BatchBuilder.php b/core/lib/Drupal/Core/Batch/BatchBuilder.php index 48eeeea298fe42117e4ae0e7cf77903948dd3a7f..f4ec34680adbc2c249219c84720ec26fa396bf9a 100644 --- a/core/lib/Drupal/Core/Batch/BatchBuilder.php +++ b/core/lib/Drupal/Core/Batch/BatchBuilder.php @@ -19,6 +19,23 @@ * } * batch_set($batch_builder->toArray()); * @endcode + * + * To prevent duplicate batches from being created inadvertently in the same + * page request, you can use ::isSetIdRegistered and ::registerSetId to check + * and see if this batch has been built before. + * @code + * if (!BatchBuilder::isSetIdRegistered('my_unique_id')) { + * $batch_builder = (new BatchBuilder()) + * ->registerSetId('my_unique_id') + * ->setTitle(t('Batch Title')) + * ->setFinishCallback('batch_example_finished_callback') + * ->setInitMessage(t('The initialization message (optional)')); + * foreach ($ids as $id) { + * $batch_builder->addOperation('batch_example_callback', [$id]); + * } + * batch_set($batch_builder->toArray()); + * } + * @endcode */ class BatchBuilder { @@ -110,6 +127,13 @@ class BatchBuilder { */ protected $queue; + /** + * A static array of custom batch ids. + * + * @var string[] + */ + protected static array $registeredSetIds = []; + /** * Sets the default values for the batch builder. */ @@ -317,6 +341,32 @@ public function addOperation(callable $callback, array $arguments = []) { return $this; } + /** + * Checks if a set ID has been registered during this request. + * + * @param string $setId + * The set ID to check. + * + * @return bool + * True if this set ID has been registered. + */ + public static function isSetIdRegistered(string $setId): bool { + return isset(static::$registeredSetIds[$setId]); + } + + /** + * Registers a set ID for this batch. + * + * @param string $setId + * The set ID to register. + * + * @return $this + */ + public function registerSetId(string $setId): self { + static::$registeredSetIds[$setId] = TRUE; + return $this; + } + /** * Converts a \Drupal\Core\Batch\Batch object into an array. * diff --git a/core/modules/node/node.module b/core/modules/node/node.module index d278c4c542fb4b67efe150fec0d2934f4622119d..7b5a4c6a40e7c31f75c327bd7cd8b4ecd53ba15b 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -532,7 +532,9 @@ function node_access_needs_rebuild($rebuild = NULL) { * has a large number of nodes). hook_update_N() and any form submit handler * are safe contexts to use the 'batch mode'. Less decidable cases (such as * calls from hook_user(), hook_taxonomy(), etc.) might consider using the - * non-batch mode. Defaults to FALSE. + * non-batch mode. Defaults to FALSE. Calling this method multiple times in + * the same request with $batch_mode set to TRUE will only result in one batch + * set being added. * * @see node_access_needs_rebuild() */ @@ -544,11 +546,14 @@ function node_access_rebuild($batch_mode = FALSE) { // Only recalculate if the site is using a node_access module. if (\Drupal::moduleHandler()->hasImplementations('node_grants')) { if ($batch_mode) { - $batch_builder = (new BatchBuilder()) - ->setTitle(t('Rebuilding content access permissions')) - ->addOperation('_node_access_rebuild_batch_operation', []) - ->setFinishCallback('_node_access_rebuild_batch_finished'); - batch_set($batch_builder->toArray()); + if (!BatchBuilder::isSetIdRegistered(__FUNCTION__)) { + $batch_builder = (new BatchBuilder()) + ->setTitle(t('Rebuilding content access permissions')) + ->addOperation('_node_access_rebuild_batch_operation', []) + ->setFinishCallback('_node_access_rebuild_batch_finished') + ->registerSetId(__FUNCTION__); + batch_set($batch_builder->toArray()); + } } else { // Try to allocate enough time to rebuild node grants diff --git a/core/modules/node/tests/src/Kernel/NodeAccessTest.php b/core/modules/node/tests/src/Kernel/NodeAccessTest.php index 741c4c20c61f9541cf0a98885a8fd55edc22fa15..62c370c8173d4169b8c2d909313ea031a8fde89a 100644 --- a/core/modules/node/tests/src/Kernel/NodeAccessTest.php +++ b/core/modules/node/tests/src/Kernel/NodeAccessTest.php @@ -156,4 +156,19 @@ public function testQueryWithBaseTableJoin(): void { $this->assertEquals(4, $query->countQuery()->execute()->fetchField()); } + /** + * Tests that multiple calls to node_access_rebuild only result in one batch. + */ + public function testDuplicateBatchRebuild(): void { + $this->enableModules(['node_access_test']); + $batch = batch_get(); + $this->assertEmpty($batch); + node_access_rebuild(TRUE); + $batch = batch_get(); + $this->assertCount(1, $batch['sets']); + node_access_rebuild(TRUE); + $batch = batch_get(); + $this->assertCount(1, $batch['sets']); + } + } diff --git a/core/tests/Drupal/Tests/Core/Batch/BatchBuilderTest.php b/core/tests/Drupal/Tests/Core/Batch/BatchBuilderTest.php index 571785ac4787dd7bc623ab90a5631c4ca634c435..f90d81658143c4d137605aa25aa65128801459d6 100644 --- a/core/tests/Drupal/Tests/Core/Batch/BatchBuilderTest.php +++ b/core/tests/Drupal/Tests/Core/Batch/BatchBuilderTest.php @@ -248,6 +248,19 @@ public function testAddOperation(): void { ], $batch['operations']); } + /** + * Tests registering IDs of built batches. + * + * @covers ::isSetIdRegistered + * @covers ::registerSetId + */ + public function testRegisterIds(): void { + $setId = $this->randomMachineName(); + $this->assertFalse(BatchBuilder::isSetIdRegistered($setId)); + (new BatchBuilder())->registerSetId($setId); + $this->assertTrue(BatchBuilder::isSetIdRegistered($setId)); + } + /** * Empty callback for the tests. *