From 7c1e3a1bb4991450c7ae661ca86317a8e4e9d3ac Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Wed, 20 Dec 2023 22:11:38 +0000 Subject: [PATCH] Issue #2183565 by plopesc, alexpott, jcnventura, DuaelFr, melvinlouwerse, grndlvl, cesarmiquel, Sweetchuck, yogeshmpawar, BetoAveiga, Sivaji_Ganesh_Jojodae, drugan, marvil07, smustgrave, poker10, catch, bbrala: Avoid loading all terms on the taxonomy overview form --- .../taxonomy/src/Form/OverviewTerms.php | 61 ++++++++++++------- .../taxonomy_test/taxonomy_test.module | 12 ++++ .../src/Functional/TaxonomyTermPagerTest.php | 45 +++++++++++++- 3 files changed, 96 insertions(+), 22 deletions(-) diff --git a/core/modules/taxonomy/src/Form/OverviewTerms.php b/core/modules/taxonomy/src/Form/OverviewTerms.php index 4e7b1f9c168d..62fc76a6b61b 100644 --- a/core/modules/taxonomy/src/Form/OverviewTerms.php +++ b/core/modules/taxonomy/src/Form/OverviewTerms.php @@ -158,7 +158,9 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular $delta = 0; $term_deltas = []; - $tree = $this->storageController->loadTree($taxonomy_vocabulary->id(), 0, NULL, TRUE); + // Terms are not loaded to avoid excessive memory consumption for large + // vocabularies. Needed terms are loaded explicitly afterward. + $tree = $this->storageController->loadTree($taxonomy_vocabulary->id(), 0, NULL, FALSE); $tree_index = 0; $complete_tree = NULL; do { @@ -179,8 +181,8 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular } // Do not let a term start the page that is not at the root. - $term = $tree[$tree_index]; - if (isset($term->depth) && ($term->depth > 0) && !isset($back_step)) { + $raw_term = $tree[$tree_index]; + if (isset($raw_term->depth) && ($raw_term->depth > 0) && !isset($back_step)) { $back_step = 0; while ($parent_term = $tree[--$tree_index]) { $before_entries--; @@ -195,7 +197,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular $back_step = $back_step ?? 0; // Continue rendering the tree until we reach the a new root item. - if ($page_entries >= $page_increment + $back_step + 1 && $term->depth == 0 && $root_entries > 1) { + if ($page_entries >= $page_increment + $back_step + 1 && $raw_term->depth == 0 && $root_entries > 1) { $complete_tree = TRUE; // This new item at the root level is the first item on the next page. $after_entries++; @@ -208,20 +210,30 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular // Finally, if we've gotten down this far, we're rendering a term on this // page. $page_entries++; - $term_deltas[$term->id()] = isset($term_deltas[$term->id()]) ? $term_deltas[$term->id()] + 1 : 0; - $key = 'tid:' . $term->id() . ':' . $term_deltas[$term->id()]; + $term_deltas[$raw_term->tid] = isset($term_deltas[$raw_term->tid]) ? $term_deltas[$raw_term->tid] + 1 : 0; + $key = 'tid:' . $raw_term->tid . ':' . $term_deltas[$raw_term->tid]; // Keep track of the first term displayed on this page. if ($page_entries == 1) { - $form['#first_tid'] = $term->id(); + $form['#first_tid'] = $raw_term->tid; } // Keep a variable to make sure at least 2 root elements are displayed. - if ($term->parents[0] == 0) { + if ($raw_term->parents[0] == 0) { $root_entries++; } - $current_page[$key] = $term; + $current_page[$key] = $raw_term; } while (isset($tree[++$tree_index])); + // Load all the terms we're going to display and set the weight and parents + // from the tree. + $terms = $this->storageController->loadMultiple(array_keys($term_deltas)); + $current_page = array_map(function ($raw_term) use ($terms) { + $term = $terms[$raw_term->tid]; + $term->depth = $raw_term->depth; + $term->parents = $raw_term->parents; + return $term; + }, $current_page); + // Because we didn't use a pager query, set the necessary pager variables. $total_entries = $before_entries + $page_entries + $after_entries; $this->pagerManager->createPager($total_entries, $page_increment); @@ -518,7 +530,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $vocabulary = $form_state->get(['taxonomy', 'vocabulary']); $changed_terms = []; - $tree = $this->storageController->loadTree($vocabulary->id(), 0, NULL, TRUE); + // Terms are not loaded to avoid excessive memory consumption for large + // vocabularies. Needed terms are loaded explicitly afterward. + $tree = $this->storageController->loadTree($vocabulary->id(), 0, NULL, FALSE); if (empty($tree)) { return; @@ -526,14 +540,14 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Build a list of all terms that need to be updated on previous pages. $weight = 0; - $term = $tree[0]; - while ($term->id() != $form['#first_tid']) { - if ($term->parents[0] == 0 && $term->getWeight() != $weight) { - $term->setWeight($weight); - $changed_terms[$term->id()] = $term; + $raw_term = $tree[0]; + $term_weights = []; + while ($raw_term->tid != $form['#first_tid']) { + if ($raw_term->parents[0] == 0 && $raw_term->weight != $weight) { + $term_weights[$raw_term->tid] = $weight; } $weight++; - $term = $tree[$weight]; + $raw_term = $tree[$weight]; } // Renumber the current page weights and assign any new parents. @@ -565,14 +579,19 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Build a list of all terms that need to be updated on following pages. for ($weight; $weight < count($tree); $weight++) { - $term = $tree[$weight]; - if ($term->parents[0] == 0 && $term->getWeight() != $weight) { - $term->parent->target_id = $term->parents[0]; - $term->setWeight($weight); - $changed_terms[$term->id()] = $term; + $raw_term = $tree[$weight]; + if ($raw_term->parents[0] == 0 && $raw_term->weight != $weight) { + $term_weights[$raw_term->tid] = $weight; } } + // Load all the items that need to be updated at once. + $terms = $this->storageController->loadMultiple(array_keys($term_weights)); + foreach ($terms as $term) { + $term->setWeight($term_weights[$term->id()]); + $changed_terms[$term->id()] = $term; + } + if (!empty($changed_terms)) { $pending_term_ids = $this->storageController->getTermIdsWithPendingRevisions(); diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test/taxonomy_test.module b/core/modules/taxonomy/tests/modules/taxonomy_test/taxonomy_test.module index e6bf91b46947..7735ea345bff 100644 --- a/core/modules/taxonomy/tests/modules/taxonomy_test/taxonomy_test.module +++ b/core/modules/taxonomy/tests/modules/taxonomy_test/taxonomy_test.module @@ -46,3 +46,15 @@ function taxonomy_test_form_taxonomy_term_form_alter(&$form, FormStateInterface $form['relations']['parent']['#disabled'] = TRUE; } } + +/** + * Implements hook_ENTITY_TYPE_load() for the taxonomy term. + */ +function taxonomy_test_taxonomy_term_load($entities) { + $value = \Drupal::state()->get(__FUNCTION__); + // Only record loaded terms is the test has set this to an empty array. + if (is_array($value)) { + $value = array_merge($value, array_keys($entities)); + \Drupal::state()->set(__FUNCTION__, array_unique($value)); + } +} diff --git a/core/modules/taxonomy/tests/src/Functional/TaxonomyTermPagerTest.php b/core/modules/taxonomy/tests/src/Functional/TaxonomyTermPagerTest.php index 5233169832b2..e46dc8ceff68 100644 --- a/core/modules/taxonomy/tests/src/Functional/TaxonomyTermPagerTest.php +++ b/core/modules/taxonomy/tests/src/Functional/TaxonomyTermPagerTest.php @@ -14,7 +14,7 @@ class TaxonomyTermPagerTest extends TaxonomyTestBase { * * @var array */ - protected static $modules = ['taxonomy']; + protected static $modules = ['taxonomy', 'taxonomy_test']; /** * {@inheritdoc} @@ -73,4 +73,47 @@ public function testTaxonomyTermOverviewPager() { $this->assertSession()->responseMatches('|<nav class="pager" [^>]*>|'); } + /** + * Tests that overview page only loads the necessary terms. + */ + public function testTaxonomyTermOverviewTermLoad() { + // Set limit to 3 terms per page. + $this->config('taxonomy.settings') + ->set('terms_per_page_admin', '3') + ->save(); + + $state = $this->container->get('state'); + + // Create 5 terms. + for ($x = 0; $x <= 10; $x++) { + $this->createTerm($this->vocabulary, ['weight' => $x]); + } + + // Check the overview page. + $state->set('taxonomy_test_taxonomy_term_load', []); + $this->drupalGet('admin/structure/taxonomy/manage/' . $this->vocabulary->id() . '/overview'); + $loaded_terms = $state->get('taxonomy_test_taxonomy_term_load'); + $this->assertCount(4, $loaded_terms); + + // Check the overview page for submit callback. + $state->set('taxonomy_test_taxonomy_term_load', []); + $this->submitForm([], 'Save'); + $loaded_terms = $state->get('taxonomy_test_taxonomy_term_load'); + $this->assertCount(4, $loaded_terms); + + $this->drupalGet('admin/structure/taxonomy/manage/' . $this->vocabulary->id() . '/overview', ['query' => ['page' => 2]]); + $state->set('taxonomy_test_taxonomy_term_load', []); + $this->submitForm([], 'Save'); + $loaded_terms = $state->get('taxonomy_test_taxonomy_term_load'); + $this->assertCount(4, $loaded_terms); + + // Adding a new term with weight < 0 implies that all root terms are updated. + $this->createTerm($this->vocabulary, ['weight' => -1]); + $this->drupalGet('admin/structure/taxonomy/manage/' . $this->vocabulary->id() . '/overview', ['query' => ['page' => 2]]); + $state->set('taxonomy_test_taxonomy_term_load', []); + $this->submitForm([], 'Save'); + $loaded_terms = $state->get('taxonomy_test_taxonomy_term_load'); + $this->assertCount(12, $loaded_terms); + } + } -- GitLab