Problem/Motivation
node_view_permissions_node_grants() has several loop inefficiencies that compound on multilingual sites with many content types:
NodeType::loadMultiple()called inside the language loop. With 8 languages, all content type entities are loaded 8 times instead of once.hasPermission()called redundantly per language. Permission results depend on the user's roles, not the language. With 8 languages and 17 types, this means ~540 redundant permission checks per invocation.- Language-independent grants set inside the language loop. Grants like
view_any_{type}_contentare identical across languages but are redundantly assigned on every language iteration. - Unused variables.
$node = \Drupal::routeMatch()->getParameter('node')and$language_managerare assigned but never used.
Related: #3571280 proposes removing language-specific grants entirely. This issue takes a less invasive approach — keeping the same grant output but restructuring the loops to avoid redundant work.
Steps to reproduce
- Install
node_view_permissionson a multilingual site (e.g., 8 languages, 17+ content types). - Profile
node_view_permissions_node_grants()with Xdebug/Blackfire. - Observe
NodeType::loadMultiple()called once per language, andhasPermission()called 4× per language/type combination instead of 4× per type.
Proposed resolution
Restructure the loops so that:
NodeType::loadMultiple()is called once, before any loop.hasPermission()results are computed once per content type (outer loop), then reused in the language loop (inner loop).- Language-independent grants are set in the outer loop, language-specific grants in the inner loop.
- Unused variables are removed.
$result = []; if ($op == 'view') { $languages = \Drupal::languageManager()->getLanguages(); $node_types = NodeType::loadMultiple(); foreach ($node_types as $type) { $type_id = $type->id(); $view_any = $account->hasPermission("view any $type_id content"); $view_own = $account->hasPermission("view own $type_id content"); $view_any_unpublished = $account->hasPermission("view any unpublished content"); $view_own_unpublished = $account->hasPermission("view own unpublished content"); // Language-independent grants (set once per type). if ($view_any) { $result["view_any_{$type_id}_content"] = [1]; } if ($view_own) { $result["view_own_{$type_id}_content"] = [$account->id()]; } if ($view_any_unpublished && $view_any) { $result["view_any_unpublished_{$type_id}_content"] = [1]; } if ($view_own_unpublished && $view_own) { $result["view_own_unpublished_{$type_id}_content"] = [$account->id()]; } // Language-specific grants (reuse cached permission results). foreach ($languages as $langcode => $language) { if ($view_any) { $result["view_any_{$type_id}_{$langcode}_content"] = [1]; } if ($view_own) { $result["view_own_{$type_id}_{$langcode}_content"] = [$account->id()]; } if ($view_any_unpublished && $view_any) { $result["view_any_unpublished_{$type_id}_{$langcode}_content"] = [1]; } if ($view_own_unpublished && $view_own) { $result["view_own_unpublished_{$type_id}_{$langcode}_content"] = [$account->id()]; } } } }
The output is identical — same grants, same keys, same values. Only the execution path changes.
Remaining tasks
- Review and test the patch.
- Verify grant output is byte-identical before and after.
Issue fork node_view_permissions-3573240
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #4
ezeedub commentedComment #5
nagy.balint commentedHi!
Shouldnt this be merged with #3573239?
I mean if we commit one of the issues the other will conflict I think.
Comment #6
ezeedub commentedI separated this one out thinking it would be simpler to get in. And this fix is independent of the other feature request.
And yes, when this lands, the other will definitely require re-basing and conflict resolution. Easy enough though.
Comment #8
nagy.balint commentedThanks!
I also added some extra tests to help with the refactor.