Problem/Motivation

As per #3091246: Allow MenuLinkTree manipulators to be altered there are a number of use cases for altering MenuLinkTree manipulators. Sometimes you just want to provide custom access controls to menu links.

One way to achieve this is to decorate the menu.default_tree_manipulators service. This works pretty well, but it means you need to implement all the public methods, including checkAccess, checkNodeAccess, generateIndexAndSort, and flatten. There is no guarantee that there won't be another method added in the future.

Steps to reproduce

mymodule.services.yml:

services:
  Drupal\my_module\Menu\MyModuleMenuLinkTreeManipulators:
    decorates: 'menu.default_tree_manipulators'
    autowire: true

MyModuleMenuLinkTreeManipulators.php

<?php

declare(strict_types=1);

namespace Drupal\my_module\Menu;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Menu\DefaultMenuLinkTreeManipulators;
use Drupal\Core\Menu\MenuLinkTreeElement;
use Drupal\Core\Session\AccountInterface;

/**
 * Decorates the menu.default_tree_manipulators service.
 *
 * @see \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators
 */
final class MyModuleMenuLinkTreeManipulators {

  public function __construct(
    protected DefaultMenuLinkTreeManipulators $inner,
    protected AccountInterface $account,
  ) {}

  /**
   * @see DefaultMenuLinkTreeManipulators::checkAccess()
   */
  public function checkAccess(array $tree): array {
    $tree = $this->inner->checkAccess($tree);
    foreach ($tree as $element) {
      $this->menuLinkCheckAccess($element);
    }
    return $tree;
  }

  /**
   * Custom access check.
   */
  protected function menuLinkCheckAccess(MenuLinkTreeElement $element): void {
    if (isset($element->access) && $element->access->isForbidden()) {
      return;
    }
    if ($element->link->getPluginId() === 'my_module.custom_menu_link') {
      $element->access = AccessResult::allowedIfHasPermission($this->account, 'my module permission');
    }
    foreach ($element->subtree as $branch) {
      $this->menuLinkCheckAccess($branch);
    }
  }

  public function checkNodeAccess(array $tree): array {
    return $this->inner->checkNodeAccess($tree);
  }

  public function generateIndexAndSort(array $tree): array {
    return $this->inner->generateIndexAndSort($tree);
  }

  public function flatten(array $tree): array {
    return $this->inner->flatten($tree);
  }

}

Proposed resolution

  • Add an interface for MenuLinkTreeManipulators
  • Split each manipulator to its own class implementing this interface

Remaining tasks

Is this even viable?

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

Comments

mstrelan created an issue.