Problem/Motivation

node_view_permissions_node_grants() has several loop inefficiencies that compound on multilingual sites with many content types:

  1. NodeType::loadMultiple() called inside the language loop. With 8 languages, all content type entities are loaded 8 times instead of once.
  2. 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.
  3. Language-independent grants set inside the language loop. Grants like view_any_{type}_content are identical across languages but are redundantly assigned on every language iteration.
  4. Unused variables. $node = \Drupal::routeMatch()->getParameter('node') and $language_manager are 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

  1. Install node_view_permissions on a multilingual site (e.g., 8 languages, 17+ content types).
  2. Profile node_view_permissions_node_grants() with Xdebug/Blackfire.
  3. Observe NodeType::loadMultiple() called once per language, and hasPermission() 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.
Command icon 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

ezeedub created an issue. See original summary.

ezeedub changed the visibility of the branch 8.x-1.x to hidden.

ezeedub’s picture

Status: Active » Needs review
nagy.balint’s picture

Hi!

Shouldnt this be merged with #3573239?

I mean if we commit one of the issues the other will conflict I think.

ezeedub’s picture

I 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.

  • nagy.balint committed 43cb7a9e on 2.x
    fix: #3573240 Optimize loop structure in hook_node_grants()
    
    By: ezeedub...
nagy.balint’s picture

Status: Needs review » Fixed

Thanks!

I also added some extra tests to help with the refactor.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.