diff --git a/core/core.services.yml b/core/core.services.yml index 72ff560296e..d34a6f2319c 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -774,6 +774,8 @@ services: menu.link_tree: class: Drupal\Core\Menu\MenuLinkTree arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@menu.active_trail', '@callable_resolver'] + tags: + - { name: service_collector, tag: menu_tree_contextual_manipulator, call: addContextualManipulator } Drupal\Core\Menu\MenuLinkTreeInterface: '@menu.link_tree' menu.default_tree_manipulators: class: Drupal\Core\Menu\DefaultMenuLinkTreeManipulators diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTranslationInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkTranslationInterface.php new file mode 100644 index 00000000000..20c223119ed --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuLinkTranslationInterface.php @@ -0,0 +1,21 @@ +callableResolver->getCallableFromDefinition($manipulator['callable']); // Prepare the arguments for the menu tree manipulator callable; the first @@ -153,6 +162,29 @@ public function transform(array $tree, array $manipulators) { $tree = call_user_func($callable, $tree); } } + + if (is_null($context)) { + @trigger_error('Transforming menu links without $context is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. See https://www.drupal.org/node/3380512', E_USER_DEPRECATED); + + return $tree; + } + + // Apply contextual menu link tree manipulators. + foreach ($this->contextualManipulators as $manipulator) { + assert($manipulator instanceof MenuLinkTreeContextualManipulatorInterface); + if (!$manipulator->applies($tree, $context)) { + continue; + } + $tree = $manipulator->process($tree, $context); + foreach ($tree as $key => $link) { + // Add manipulator as a cacheable dependency to all link tree elements + // to make sure cacheable metadata bubbles up. + if (!$tree[$key]->access) { + $tree[$key]->access = new AccessResultNeutral(); + } + $tree[$key]->access->addCacheableDependency($manipulator); + } + } return $tree; } @@ -308,4 +340,16 @@ public function getExpanded($menu_name, array $parents) { return $this->treeStorage->getExpanded($menu_name, $parents); } + /** + * Add a manipulator to the list of manipulators. + * + * @param \Drupal\Core\Menu\MenuLinkTreeContextualManipulatorInterface $manipulator + * A menu link tree manipulator. + * + * @return void + */ + public function addContextualManipulator(MenuLinkTreeContextualManipulatorInterface $manipulator): void { + $this->contextualManipulators[] = $manipulator; + } + } diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTreeContextualManipulatorInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkTreeContextualManipulatorInterface.php new file mode 100644 index 00000000000..e6b3820d566 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuLinkTreeContextualManipulatorInterface.php @@ -0,0 +1,45 @@ + 'menu.default_tree_manipulators:checkAccess'], ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], ]; - $tree = $this->menuLinkTree->transform($tree, $manipulators); + $tree = $this->menuLinkTree->transform($tree, $manipulators, $this); $this->parentSelectOptionsTreeWalk($tree, $menu_name, '--', $options, $id, $depth_limit, $cacheability); } return $options; diff --git a/core/lib/Drupal/Core/Menu/menu.api.php b/core/lib/Drupal/Core/Menu/menu.api.php index 4d775978c22..d21c7793250 100644 --- a/core/lib/Drupal/Core/Menu/menu.api.php +++ b/core/lib/Drupal/Core/Menu/menu.api.php @@ -97,6 +97,21 @@ * @todo Derivatives are in flux for these; when they are more stable, add * documentation here. * + * @section Menu Link Tree Manipulators + * When rendering menu link trees, the menu links are transformed into an actual + * link tree that is ready for output. There are two categories of menu link + * tree manipulators involved in this process: + * + * Explicit Manipulators: These are deliberately added during rendering of the + * menu link tree. For example, access checking is commonly done using an + * explicit manipulator. + * + * Contextual Manipulators: Introduced by other modules, these manipulators are + * added based on the context where the links are rendered. These are usually + * needed for side effects that a module needs to introduce. For example, a + * module might add a manipulator to remove untranslated links from the menu + * link tree. + * * @section sec_actions Defining local actions for routes * Local actions can be defined for operations related to a given route. For * instance, adding content is a common operation for the content management diff --git a/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php index 4c07d4d9ccf..d7c0d55fea3 100644 --- a/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php +++ b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php @@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Menu\MenuLinkBase; +use Drupal\Core\Menu\MenuLinkTranslationInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\menu_link_content\MenuLinkContentInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -14,7 +15,7 @@ /** * Provides the menu link plugin for content menu links. */ -class MenuLinkContent extends MenuLinkBase implements ContainerFactoryPluginInterface { +class MenuLinkContent extends MenuLinkBase implements ContainerFactoryPluginInterface, MenuLinkTranslationInterface { /** * Entities IDs to load. @@ -280,6 +281,13 @@ public function isTranslatable() { return $this->getEntity()->isTranslatable(); } + /** + * {@inheritdoc} + */ + public function hasTranslation(string $langcode): bool { + return $this->getEntity()->hasTranslation($langcode); + } + /** * {@inheritdoc} */ diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php index 9d90ac5a607..3cb0f4c18f4 100644 --- a/core/modules/menu_ui/src/MenuForm.php +++ b/core/modules/menu_ui/src/MenuForm.php @@ -234,7 +234,7 @@ protected function buildOverviewForm(array &$form, FormStateInterface $form_stat ['callable' => 'menu.default_tree_manipulators:checkAccess'], ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], ]; - $tree = $this->menuTree->transform($tree, $manipulators); + $tree = $this->menuTree->transform($tree, $manipulators, $this); $this->getRequest()->attributes->set('_menu_admin', FALSE); // Determine the delta; the number of weights to be made available. diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index a495c9cf7af..fd20a4626bf 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -386,6 +386,10 @@ block.settings.system_menu_block:*: expand_all_items: type: boolean label: 'Expand all items' + hide_untranslated_menu_links: + type: boolean + label: Hide untranslated custom menu links + nullable: true block.settings.local_tasks_block: type: block_settings diff --git a/core/modules/system/src/Controller/LinksetController.php b/core/modules/system/src/Controller/LinksetController.php index 793040adb06..716f0c7371f 100644 --- a/core/modules/system/src/Controller/LinksetController.php +++ b/core/modules/system/src/Controller/LinksetController.php @@ -275,7 +275,7 @@ protected function loadMenuTree(MenuInterface $menu) : array { ['callable' => 'menu.default_tree_manipulators:checkAccess'], ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], ]; - return $this->menuTree->transform($tree, $manipulators); + return $this->menuTree->transform($tree, $manipulators, $this); } } diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php index afb715dad9a..fa32903b1f0 100644 --- a/core/modules/system/src/Controller/SystemController.php +++ b/core/modules/system/src/Controller/SystemController.php @@ -131,7 +131,7 @@ public function overview($link_id) { ['callable' => 'menu.default_tree_manipulators:checkAccess'], ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], ]; - $tree = $this->menuLinkTree->transform($tree, $manipulators); + $tree = $this->menuLinkTree->transform($tree, $manipulators, $this); $tree_access_cacheability = new CacheableMetadata(); $blocks = []; foreach ($tree as $key => $element) { diff --git a/core/modules/system/src/Menu/LanguageMenuLinkTreeManipulator.php b/core/modules/system/src/Menu/LanguageMenuLinkTreeManipulator.php new file mode 100644 index 00000000000..e403e98936b --- /dev/null +++ b/core/modules/system/src/Menu/LanguageMenuLinkTreeManipulator.php @@ -0,0 +1,95 @@ +getCurrentLanguage()->getId(); + foreach ($tree as $key => $link) { + if (!$link->link instanceof MenuLinkTranslationInterface) { + continue; + } + + if ($link->link->isTranslatable() && !$link->link->hasTranslation($current_language)) { + $access = new AccessResultForbidden(); + + // Instead of removing the menu link, mark it inaccessible so that its + // cacheable metadata bubbles up. + // @see \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators::checkAccess + if ($tree[$key]->access instanceof AccessResultInterface) { + $access = $tree[$key]->access->andIf($access); + } + $tree[$key]->access = $access; + $tree[$key]->link = new InaccessibleMenuLink($link->link); + $tree[$key]->subtree = []; + } + elseif ($link->hasChildren) { + // Recursively call this method to filter out untranslated children. + $tree[$key]->subtree = $this->process($link->subtree, $context); + } + } + return $tree; + } + + /** + * {@inheritdoc} + */ + public function applies(array $tree, mixed $context): bool { + if (!($context instanceof SystemMenuBlock)) { + return FALSE; + } + + $configuration = $context->getConfiguration(); + if (!isset($configuration['hide_untranslated_menu_links']) || $configuration['hide_untranslated_menu_links'] !== TRUE) { + return FALSE; + } + + return TRUE; + } + + /** + * Gets the current language. + * + * @return \Drupal\Core\Language\LanguageInterface + * The current language. + */ + protected function getCurrentLanguage(): LanguageInterface { + return $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT); + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts(): array { + return ['languages:language_content']; + } + +} diff --git a/core/modules/system/src/ModuleAdminLinksHelper.php b/core/modules/system/src/ModuleAdminLinksHelper.php index 27310cf107a..04d3b64b0e4 100644 --- a/core/modules/system/src/ModuleAdminLinksHelper.php +++ b/core/modules/system/src/ModuleAdminLinksHelper.php @@ -58,7 +58,7 @@ public function getModuleAdminLinks(string $module): array { ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], ['callable' => 'menu.default_tree_manipulators:flatten'], ]; - $menuTree = $this->menuLinkTree->transform($menuTree, $manipulators); + $menuTree = $this->menuLinkTree->transform($menuTree, $manipulators, $this); $this->memoryCache->set(self::ADMIN_LINKS_MENU_TREE, $menuTree); } diff --git a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php index 8918d4b2afa..314a848bc33 100644 --- a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php @@ -6,6 +6,7 @@ use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\Cache; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Menu\MenuActiveTrailInterface; use Drupal\Core\Menu\MenuLinkTreeInterface; use Drupal\Core\Menu\MenuTreeParameters; @@ -56,11 +57,18 @@ class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterfa * The menu tree service. * @param \Drupal\Core\Menu\MenuActiveTrailInterface $menu_active_trail * The active menu trail service. + * @param \Drupal\Core\Language\LanguageManagerInterface|null $languageManager + * The language manager service. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MenuLinkTreeInterface $menu_tree, MenuActiveTrailInterface $menu_active_trail) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, MenuLinkTreeInterface $menu_tree, MenuActiveTrailInterface $menu_active_trail, protected ?LanguageManagerInterface $languageManager = NULL) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->menuTree = $menu_tree; $this->menuActiveTrail = $menu_active_trail; + + if ($this->languageManager === NULL) { + @trigger_error('Calling SystemMenuBlock::__construct() without the $languageManager argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3392737', E_USER_DEPRECATED); + $this->languageManager = \Drupal::service('language_manager'); + } } /** @@ -72,7 +80,8 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_id, $plugin_definition, $container->get('menu.link_tree'), - $container->get('menu.active_trail') + $container->get('menu.active_trail'), + $container->get('language_manager') ); } @@ -121,6 +130,14 @@ public function blockForm($form, FormStateInterface $form_state) { '#description' => $this->t('Override the option found on each menu link used for expanding children and instead display the whole menu tree as expanded.'), ]; + $form['hide_untranslated_menu_links'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Hide untranslated custom menu links'), + '#description' => $this->t('When selected, custom menu links without translation in the current language are hidden.'), + '#default_value' => !empty($config['hide_untranslated_menu_links']), + '#access' => $this->languageManager->isMultilingual(), + ]; + return $form; } @@ -141,6 +158,7 @@ public function blockSubmit($form, FormStateInterface $form_state) { $this->configuration['level'] = $form_state->getValue('level'); $this->configuration['depth'] = $form_state->getValue('depth'); $this->configuration['expand_all_items'] = $form_state->getValue('expand_all_items'); + $this->configuration['hide_untranslated_menu_links'] = $form_state->getValue('hide_untranslated_menu_links'); } /** @@ -193,7 +211,7 @@ public function build() { ['callable' => 'menu.default_tree_manipulators:checkAccess'], ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], ]; - $tree = $this->menuTree->transform($tree, $manipulators); + $tree = $this->menuTree->transform($tree, $manipulators, $this); return $this->menuTree->build($tree); } diff --git a/core/modules/system/src/SystemManager.php b/core/modules/system/src/SystemManager.php index 7c88d95c3e7..9fe6418981d 100644 --- a/core/modules/system/src/SystemManager.php +++ b/core/modules/system/src/SystemManager.php @@ -191,7 +191,7 @@ public function getAdminBlock(MenuLinkInterface $instance) { ['callable' => 'menu.default_tree_manipulators:checkAccess'], ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], ]; - $tree = $this->menuTree->transform($tree, $manipulators); + $tree = $this->menuTree->transform($tree, $manipulators, $this); foreach ($tree as $key => $element) { // Only render accessible links. if (!$element->access->isAllowed()) { diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml index 7754d143728..90324df0b93 100644 --- a/core/modules/system/system.services.yml +++ b/core/modules/system/system.services.yml @@ -86,3 +86,8 @@ services: class: Drupal\system\EventSubscriber\AccessRouteAlterSubscriber tags: - { name: event_subscriber } + menu.language_menu_link_tree_manipulator: + class: Drupal\system\Menu\LanguageMenuLinkTreeManipulator + arguments: ['@language_manager'] + tags: + - { name: menu_tree_contextual_manipulator } diff --git a/core/modules/system/tests/src/Functional/System/AdminTest.php b/core/modules/system/tests/src/Functional/System/AdminTest.php index 8f72a98ec74..92e55611f8b 100644 --- a/core/modules/system/tests/src/Functional/System/AdminTest.php +++ b/core/modules/system/tests/src/Functional/System/AdminTest.php @@ -137,7 +137,7 @@ protected function getTopLevelMenuLinks() { ['callable' => 'menu.default_tree_manipulators:checkAccess'], ['callable' => 'menu.default_tree_manipulators:flatten'], ]; - $tree = $menu_tree->transform($tree, $manipulators); + $tree = $menu_tree->transform($tree, $manipulators, $this); // Transform the tree to a list of menu links. $menu_links = []; diff --git a/core/modules/system/tests/src/Kernel/Plugin/Block/SystemMenuBlockTranslationTest.php b/core/modules/system/tests/src/Kernel/Plugin/Block/SystemMenuBlockTranslationTest.php new file mode 100644 index 00000000000..664f74360b5 --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Plugin/Block/SystemMenuBlockTranslationTest.php @@ -0,0 +1,132 @@ +installConfig(['language']); + $this->installEntitySchema('configurable_language'); + $this->installEntitySchema('user'); + $this->installEntitySchema('menu_link_content'); + + // Add custom menu. + $this->menu = Menu::create([ + 'id' => 'mock', + 'label' => $this->randomMachineName(16), + 'description' => 'Description text', + ]); + $this->menu->save(); + + // Make menu content links translatable. + $this->container->get('content_translation.manager')->setEnabled('menu_link_content', 'menu_link_content', TRUE); + } + + /** + * Tests that menu blocks display links in proper language. + * + * @covers \Drupal\system\Menu\LanguageMenuLinkTreeManipulator::process + */ + public function testMenuBlockTranslation(): void { + $fr_language = ConfigurableLanguage::createFromLangcode('fr'); + $fr_language->save(); + // Create menu links in each language. + $languages = [ + 'en' => 'English', + 'fr' => 'French', + Language::LANGCODE_NOT_SPECIFIED => 'Not specified', + Language::LANGCODE_NOT_APPLICABLE => 'Not applicable', + ]; + $links = []; + foreach ($languages as $langcode => $langname) { + $link = MenuLinkContent::create([ + 'title' => "test $langname", + 'link' => ['uri' => 'https://www.drupal.org/'], + 'menu_name' => $this->menu->id(), + 'external' => TRUE, + 'bundle' => 'menu_link_content', + 'langcode' => $langcode, + ]); + $link->save(); + $links[$langcode] = $link; + } + + /** @var \Drupal\Core\Block\BlockPluginInterface $block */ + $block = $this->container->get('plugin.manager.block')->createInstance('system_menu_block:' . $this->menu->id()); + $block->setConfigurationValue('hide_untranslated_menu_links', TRUE); + + // If English is the default language the items that should be set are + // - English + // - Not specified + // - Not applicable + // It should not show French links. + $build = $block->build(); + $this->assertArrayHasKey('languages:language_content', array_flip($build['#cache']['contexts'])); + $this->assertArrayHasKey('#items', $build); + $this->assertArrayHasKey($links['en']->getPluginId(), $build['#items']); + $this->assertArrayNotHasKey($links['fr']->getPluginId(), $build['#items']); + $this->assertArrayHasKey($links[Language::LANGCODE_NOT_SPECIFIED]->getPluginId(), $build['#items']); + $this->assertArrayHasKey($links[Language::LANGCODE_NOT_APPLICABLE]->getPluginId(), $build['#items']); + + // If French is the default language the items that should be set are + // - French + // - Not specified + // - Not applicable + // It should not show English links. + $this->container->get('language.default')->set($fr_language); + $this->container->get('language_manager')->reset(); + $build = $block->build(); + $this->assertArrayHasKey('languages:language_content', array_flip($build['#cache']['contexts'])); + $this->assertArrayHasKey('#items', $build); + $this->assertArrayNotHasKey($links['en']->getPluginId(), $build['#items']); + $this->assertArrayHasKey($links['fr']->getPluginId(), $build['#items']); + $this->assertArrayHasKey($links[Language::LANGCODE_NOT_SPECIFIED]->getPluginId(), $build['#items']); + $this->assertArrayHasKey($links[Language::LANGCODE_NOT_APPLICABLE]->getPluginId(), $build['#items']); + } + +} diff --git a/core/modules/toolbar/src/Controller/ToolbarController.php b/core/modules/toolbar/src/Controller/ToolbarController.php index c683494f505..ef6e558dbe4 100644 --- a/core/modules/toolbar/src/Controller/ToolbarController.php +++ b/core/modules/toolbar/src/Controller/ToolbarController.php @@ -80,7 +80,7 @@ public static function preRenderAdministrationTray(array $element) { ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], ['callable' => 'toolbar_menu_navigation_links'], ]; - $tree = $menu_tree->transform($tree, $manipulators); + $tree = $menu_tree->transform($tree, $manipulators, __METHOD__); $element['administration_menu'] = $menu_tree->build($tree); return $element; } @@ -105,7 +105,7 @@ public static function preRenderGetRenderedSubtrees(array $data) { ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], ['callable' => 'toolbar_menu_navigation_links'], ]; - $tree = $menu_tree->transform($tree, $manipulators); + $tree = $menu_tree->transform($tree, $manipulators, __METHOD__); $subtrees = []; // Calculated the combined cacheability of all subtrees. $cacheability = CacheableMetadata::createFromRenderArray($data); diff --git a/core/tests/Drupal/KernelTests/Core/DependencyInjection/AutowireTest.php b/core/tests/Drupal/KernelTests/Core/DependencyInjection/AutowireTest.php index 0ba24038453..bda79197cf6 100644 --- a/core/tests/Drupal/KernelTests/Core/DependencyInjection/AutowireTest.php +++ b/core/tests/Drupal/KernelTests/Core/DependencyInjection/AutowireTest.php @@ -68,6 +68,7 @@ public function testCoreServiceAliases(): void { 'context_provider', 'event_subscriber', 'module_install.uninstall_validator', + 'menu_tree_contextual_manipulator', ])) { continue 2; } diff --git a/core/tests/Drupal/KernelTests/Core/Menu/MenuLinkTreeTest.php b/core/tests/Drupal/KernelTests/Core/Menu/MenuLinkTreeTest.php index c36c37fea91..23f7827bd59 100644 --- a/core/tests/Drupal/KernelTests/Core/Menu/MenuLinkTreeTest.php +++ b/core/tests/Drupal/KernelTests/Core/Menu/MenuLinkTreeTest.php @@ -2,6 +2,7 @@ namespace Drupal\KernelTests\Core\Menu; +use Drupal\Core\Menu\MenuLinkTreeContextualManipulatorInterface; use Drupal\Core\Menu\MenuLinkTreeElement; use Drupal\Core\Menu\MenuTreeParameters; use Drupal\KernelTests\KernelTestBase; @@ -133,4 +134,53 @@ public function testCreateLinksInMenu() { $this->assertEquals(3, $height); } + /** + * Tests transforming a link tree from a contextual manipulator. + */ + public function testContextualManipulator() { + /** @var \Drupal\system\MenuStorage $storage */ + $storage = \Drupal::entityTypeManager()->getStorage('menu'); + $storage->create(['id' => 'menu1', 'label' => 'Menu 1'])->save(); + + \Drupal::entityTypeManager()->getStorage('menu_link_content')->create(['link' => ['uri' => 'internal:/menu_name_test'], 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'title' => 'Link test'])->save(); + \Drupal::entityTypeManager()->getStorage('menu_link_content')->create(['link' => ['uri' => 'internal:/menu_name_test'], 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'title' => 'Link test'])->save(); + $output = $this->linkTree->load('menu1', new MenuTreeParameters()); + $this->assertCount(2, $output); + + // Add contextual manipulator that applies. + $context = new \stdClass(); + $contextual_manipulator = $this->prophesize(MenuLinkTreeContextualManipulatorInterface::class); + $contextual_manipulator->applies($output, $context)->willReturn(TRUE); + $contextual_manipulator->process($output, $context)->willReturn([$output[array_key_first($output)]]); + $contextual_manipulator->getCacheContexts()->willReturn([]); + $contextual_manipulator->getCacheTags()->willReturn([]); + $contextual_manipulator->getCacheMaxAge()->willReturn(1337); + $this->linkTree->addContextualManipulator($contextual_manipulator->reveal()); + + // Add contextual manipulator that doesn't apply. + $context = new \stdClass(); + $non_applicable_manipulator = $this->prophesize(MenuLinkTreeContextualManipulatorInterface::class); + $non_applicable_manipulator->applies([$output[array_key_first($output)]], $context)->willReturn(FALSE); + $this->linkTree->addContextualManipulator($non_applicable_manipulator->reveal()); + + $this->assertCount(1, $this->linkTree->transform($output, [], $context)); + + $render_array = $this->linkTree->build($output); + $this->assertSame(1337, $render_array['#cache']['max-age']); + } + + /** + * Tests transforming a link tree. + * + * @group legacy + */ + public function testTransformWithoutContext() { + /** @var \Drupal\system\MenuStorage $storage */ + $storage = \Drupal::entityTypeManager()->getStorage('menu'); + $storage->create(['id' => 'menu1', 'label' => 'Menu 1'])->save(); + $output = $this->linkTree->load('menu1', new MenuTreeParameters()); + $this->expectDeprecation('Transforming menu links without $context is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. See https://www.drupal.org/node/3380512'); + $this->linkTree->transform($output, []); + } + }