From 623a2ed607119891c185f9704d57335b91100943 Mon Sep 17 00:00:00 2001 From: Toni Sinisalo Date: Fri, 13 Feb 2026 16:42:58 +0200 Subject: [PATCH 1/9] UHF-12760: Adding group menu on group news items. --- .../src/HelfiGroupServiceProvider.php | 26 ++++ .../src/Menu/GroupMenuActiveTrail.php | 110 +++++++++++++ .../ProxyClass/Menu/GroupMenuActiveTrail.php | 144 ++++++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 public/modules/custom/helfi_group/src/HelfiGroupServiceProvider.php create mode 100644 public/modules/custom/helfi_group/src/Menu/GroupMenuActiveTrail.php create mode 100644 public/modules/custom/helfi_group/src/ProxyClass/Menu/GroupMenuActiveTrail.php diff --git a/public/modules/custom/helfi_group/src/HelfiGroupServiceProvider.php b/public/modules/custom/helfi_group/src/HelfiGroupServiceProvider.php new file mode 100644 index 00000000..83fa31c9 --- /dev/null +++ b/public/modules/custom/helfi_group/src/HelfiGroupServiceProvider.php @@ -0,0 +1,26 @@ +getDefinition('menu.active_trail'); + $definition->setClass('Drupal\helfi_group\Menu\GroupMenuActiveTrail'); + $definition + ->addArgument(new Reference('group.group_route_context')); + } + +} diff --git a/public/modules/custom/helfi_group/src/Menu/GroupMenuActiveTrail.php b/public/modules/custom/helfi_group/src/Menu/GroupMenuActiveTrail.php new file mode 100644 index 00000000..82c72ffb --- /dev/null +++ b/public/modules/custom/helfi_group/src/Menu/GroupMenuActiveTrail.php @@ -0,0 +1,110 @@ +groupRouteContext = $group_route_context; + } + + /** + * {@inheritdoc} + * + * Handles active trail for group news items, which are not included in + * the menu tree. Instead, the active trail is built from the news parent + * node. + */ + public function getActiveLink($menu_name = NULL) { + // If the menu name is not a group menu, use the default implementation. + if (!str_starts_with($menu_name, 'group_menu_link_content-')) { + return parent::getActiveLink($menu_name); + } + + // If this is not a news item, use the default implementation. + $node = $this->routeMatch->getParameter('node'); + if (!$node || !($node instanceof NodeInterface) || $node->bundle() !== 'news_item') { + return parent::getActiveLink($menu_name); + } + + // If no group is found, use the default implementation. + $group = $this->groupRouteContext->getBestCandidate(); + if (!$group) { + return parent::getActiveLink($menu_name); + } + + // If no news parent is found, use the default implementation. + $newsParent = NULL; + if ($group->hasField('field_group_news_parent')) { + $newsParent = $group->get('field_group_news_parent')->entity; + } + if (!$newsParent || !($newsParent instanceof NodeInterface)) { + return parent::getActiveLink($menu_name); + } + + // The menu links coming from the storage are already sorted by depth, + // weight and ID. + $found = NULL; + $links = []; + + $route_name = $newsParent->toUrl()->getRouteName(); + if ($route_name) { + $route_parameters = $newsParent->toUrl()->getRouteParameters(); + + // Load links matching this route. + $links = $this->menuLinkManager->loadLinksByRoute($route_name, $route_parameters, $menu_name); + } + + // Select the first matching link. + if ($links) { + $found = reset($links); + } + return $found; + } + +} diff --git a/public/modules/custom/helfi_group/src/ProxyClass/Menu/GroupMenuActiveTrail.php b/public/modules/custom/helfi_group/src/ProxyClass/Menu/GroupMenuActiveTrail.php new file mode 100644 index 00000000..ee1de15b --- /dev/null +++ b/public/modules/custom/helfi_group/src/ProxyClass/Menu/GroupMenuActiveTrail.php @@ -0,0 +1,144 @@ +container = $container; + $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id; + } + + /** + * Lazy loads the real service from the container. + * + * @return object + * Returns the constructed real service. + */ + protected function lazyLoadItself() + { + if (!isset($this->service)) { + $this->service = $this->container->get($this->drupalProxyOriginalServiceId); + } + + return $this->service; + } + + /** + * {@inheritdoc} + */ + public function getActiveLink($menu_name = NULL) + { + return $this->lazyLoadItself()->getActiveLink($menu_name); + } + + /** + * {@inheritdoc} + */ + public function getActiveTrailIds($menu_name) + { + return $this->lazyLoadItself()->getActiveTrailIds($menu_name); + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + return $this->lazyLoadItself()->has($key); + } + + /** + * {@inheritdoc} + */ + public function get($key) + { + return $this->lazyLoadItself()->get($key); + } + + /** + * {@inheritdoc} + */ + public function set($key, $value) + { + return $this->lazyLoadItself()->set($key, $value); + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + return $this->lazyLoadItself()->delete($key); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + return $this->lazyLoadItself()->reset(); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return $this->lazyLoadItself()->clear(); + } + + /** + * {@inheritdoc} + */ + public function destruct() + { + return $this->lazyLoadItself()->destruct(); + } + + } + +} From a444029e36e457ae0f65b7094dc72c244df9f204 Mon Sep 17 00:00:00 2001 From: Toni Sinisalo Date: Mon, 16 Feb 2026 12:03:44 +0200 Subject: [PATCH 2/9] UHF-12760: Updating test dependencies. --- .../Kernel/EventSubscriber/GroupMenuFilterByLanguageTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/modules/custom/helfi_group/tests/src/Kernel/EventSubscriber/GroupMenuFilterByLanguageTest.php b/public/modules/custom/helfi_group/tests/src/Kernel/EventSubscriber/GroupMenuFilterByLanguageTest.php index b22a2cd7..40b67605 100644 --- a/public/modules/custom/helfi_group/tests/src/Kernel/EventSubscriber/GroupMenuFilterByLanguageTest.php +++ b/public/modules/custom/helfi_group/tests/src/Kernel/EventSubscriber/GroupMenuFilterByLanguageTest.php @@ -43,9 +43,12 @@ class GroupMenuFilterByLanguageTest extends EntityKernelTestBase { 'content_translation', 'link', 'helfi_group', + 'helfi_tpr', 'menu_block_current_language', 'language', 'locale', + 'flexible_permissions', + 'group', ]; /** From eb43de65fc4990c6848a1bf931da7b4de0adaae2 Mon Sep 17 00:00:00 2001 From: Toni Sinisalo Date: Tue, 17 Feb 2026 10:13:30 +0200 Subject: [PATCH 3/9] UHF-12760: Adding tests. --- .../src/HelfiGroupServiceProvider.php | 11 +- .../Kernel/HelfiGroupServiceProviderTest.php | 35 +++ .../Unit/Menu/GroupMenuActiveTrailTest.php | 204 ++++++++++++++++++ 3 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 public/modules/custom/helfi_group/tests/src/Kernel/HelfiGroupServiceProviderTest.php create mode 100644 public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailTest.php diff --git a/public/modules/custom/helfi_group/src/HelfiGroupServiceProvider.php b/public/modules/custom/helfi_group/src/HelfiGroupServiceProvider.php index 83fa31c9..460efe52 100644 --- a/public/modules/custom/helfi_group/src/HelfiGroupServiceProvider.php +++ b/public/modules/custom/helfi_group/src/HelfiGroupServiceProvider.php @@ -6,6 +6,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DependencyInjection\ServiceProviderBase; +use Drupal\helfi_group\Menu\GroupMenuActiveTrail; use Symfony\Component\DependencyInjection\Reference; /** @@ -17,10 +18,12 @@ class HelfiGroupServiceProvider extends ServiceProviderBase { * {@inheritdoc} */ public function alter(ContainerBuilder $container) { - $definition = $container->getDefinition('menu.active_trail'); - $definition->setClass('Drupal\helfi_group\Menu\GroupMenuActiveTrail'); - $definition - ->addArgument(new Reference('group.group_route_context')); + if ($container->hasDefinition('menu.active_trail')) { + $definition = $container->getDefinition('menu.active_trail'); + $definition->setClass(GroupMenuActiveTrail::class); + $definition + ->addArgument(new Reference('group.group_route_context')); + } } } diff --git a/public/modules/custom/helfi_group/tests/src/Kernel/HelfiGroupServiceProviderTest.php b/public/modules/custom/helfi_group/tests/src/Kernel/HelfiGroupServiceProviderTest.php new file mode 100644 index 00000000..9ce6b9aa --- /dev/null +++ b/public/modules/custom/helfi_group/tests/src/Kernel/HelfiGroupServiceProviderTest.php @@ -0,0 +1,35 @@ +alter($this->container); + $definition = $this->container->getDefinition('menu.active_trail'); + $this->assertEquals(GroupMenuActiveTrail::class, $definition->getClass()); + } + +} diff --git a/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailTest.php b/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailTest.php new file mode 100644 index 00000000..f4e0f8b8 --- /dev/null +++ b/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailTest.php @@ -0,0 +1,204 @@ + + */ + protected ObjectProphecy $menuLinkManager; + + /** + * The mocked route match. + * + * @var \Prophecy\Prophecy\ObjectProphecy + */ + protected ObjectProphecy $routeMatch; + + /** + * The mocked group route context. + * + * @var \Prophecy\Prophecy\ObjectProphecy + */ + protected ObjectProphecy $groupRouteContext; + + /** + * The mocked route match node. + * + * @var \Prophecy\Prophecy\ObjectProphecy + */ + protected ObjectProphecy $node; + + /** + * The mocked group. + * + * @var \Prophecy\Prophecy\ObjectProphecy + */ + protected ObjectProphecy $group; + + /** + * The mocked news parent node. + * + * @var \Prophecy\Prophecy\ObjectProphecy + */ + protected ObjectProphecy $newsParentNode; + + /** + * SUT instance. + * + * @var \Drupal\helfi_group\Menu\GroupMenuActiveTrail + */ + protected GroupMenuActiveTrail $groupMenuActiveTrail; + + /** + * {@inheritdoc} + */ + public function setUp(): void { + parent::setUp(); + + $testMenuLink = MenuLinkMock::createMock([ + 'id' => 'test_menu_link', + 'route_name' => 'test.route.name', + 'title' => 'Test Menu Link', + 'parent' => NULL, + ]); + + $groupTestMenuLink = MenuLinkMock::createMock([ + 'id' => 'group_test_menu_link', + 'route_name' => 'group_test.route.name', + 'title' => 'Group Test Menu Link', + 'parent' => NULL, + ]); + + $this->menuLinkManager = $this->prophesize(MenuLinkManagerInterface::class); + $this->menuLinkManager->loadLinksByRoute('test.route.name', Argument::any(), Argument::any())->willReturn([ + $testMenuLink, + ]); + $this->menuLinkManager->loadLinksByRoute('group_test.route.name', Argument::any(), Argument::any())->willReturn([ + $groupTestMenuLink, + ]); + + $parameterBag = $this->prophesize(ParameterBag::class); + $parameterBag->all()->willReturn([]); + + $this->node = $this->prophesize(NodeInterface::class); + + $url = $this->prophesize(Url::class); + $url->getRouteName()->willReturn('group_test.route.name'); + $url->getRouteParameters()->willReturn([]); + $this->newsParentNode = $this->prophesize(NodeInterface::class); + $this->newsParentNode->toUrl()->willReturn($url->reveal()); + + $newsParentField = (object) ['entity' => $this->newsParentNode->reveal()]; + + $this->group = $this->prophesize(GroupInterface::class); + $this->group->get('field_group_news_parent')->willReturn($newsParentField); + + $this->routeMatch = $this->prophesize(RouteMatchInterface::class); + $this->routeMatch->getRouteName()->willReturn('test.route.name'); + $this->routeMatch->getRawParameters()->willReturn($parameterBag->reveal()); + $this->routeMatch->getParameter('node')->willReturn($this->node->reveal()); + + $this->groupRouteContext = $this->prophesize(GroupRouteContext::class); + + $this->groupMenuActiveTrail = new GroupMenuActiveTrail( + $this->menuLinkManager->reveal(), + $this->routeMatch->reveal(), + $this->prophesize(CacheBackendInterface::class)->reveal(), + $this->prophesize(LockBackendInterface::class)->reveal(), + $this->prophesize(PathMatcherInterface::class)->reveal(), + $this->groupRouteContext->reveal(), + ); + } + + /** + * Data provider for the getActiveLink method. + * + * @return array + * Test data. + */ + public static function getActiveLinkDataProvider() { + $data = []; + + // Not a group menu. + $data['not a group menu'] = [ + 'menu_name' => 'not_a_group_menu', + ]; + + // Not a news item. + $data['not a news item'] = [ + 'node_bundle' => 'not_news_item', + ]; + + // No group found. + $data['no group found'] = [ + 'group_found' => FALSE, + ]; + + // No news parent found. + $data['no news parent found'] = [ + 'news_parent_found' => FALSE, + ]; + + // News parent found. + $data['news parent found'] = [ + 'expected_link_title' => 'Group Test Menu Link', + ]; + + return $data; + } + + /** + * Tests the getActiveLink method. + * + * @param string $menu_name + * The menu name. + * @param string $expected_link_title + * The expected link title. + * @param string $node_bundle + * The current route match node bundle. + * @param bool $group_found + * Whether the group was found. + * @param bool $news_parent_found + * Whether the news parent was found. + */ + #[DataProvider('getActiveLinkDataProvider')] + public function testGetActiveLink( + $menu_name = 'group_menu_link_content-1', + $expected_link_title = 'Test Menu Link', + $node_bundle = 'news_item', + $group_found = TRUE, + $news_parent_found = TRUE, + ) { + $this->node->bundle()->willReturn($node_bundle); + $this->groupRouteContext->getBestCandidate()->willReturn($group_found ? $this->group->reveal() : NULL); + $this->group->hasField('field_group_news_parent')->willReturn($news_parent_found); + $this->assertEquals($expected_link_title, $this->groupMenuActiveTrail->getActiveLink($menu_name)->getTitle()); + } + +} From 1a78ca9e59ac09731c61a5638d5a1a441c411ccc Mon Sep 17 00:00:00 2001 From: Toni Sinisalo Date: Tue, 17 Feb 2026 15:41:54 +0200 Subject: [PATCH 4/9] UHF-12760: Forcing a path alias update on all news items to make sure they use the proper alias pattern. --- .../custom/helfi_group/helfi_group.install | 54 +++++++++++++++++++ .../custom/helfi_group/helfi_group.tokens.inc | 11 +++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/public/modules/custom/helfi_group/helfi_group.install b/public/modules/custom/helfi_group/helfi_group.install index ba9ba29e..1a2712b5 100644 --- a/public/modules/custom/helfi_group/helfi_group.install +++ b/public/modules/custom/helfi_group/helfi_group.install @@ -7,9 +7,11 @@ declare(strict_types=1); +use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Url; use Drupal\menu_link_content\Entity\MenuLinkContent; use Drupal\node\Entity\Node; +use Drupal\pathauto\PathautoState; /** * Update group menu item's content translation status to match node's status. @@ -52,3 +54,55 @@ function helfi_group_update_9001(): void { } } } + +/** + * Update path aliases for group news items. + */ +function helfi_group_update_9002(&$sandbox): void { + $query = \Drupal::entityQuery('node') + ->condition('type', 'news_item') + ->accessCheck(FALSE); + $result = $query->execute(); + + if (!$result) { + return; + } + + // Use the sandbox to store the information needed to track progression. + if (!isset($sandbox['current'])) { + // The count of entities visited so far. + $sandbox['current'] = 0; + // Total entities that must be visited. + $sandbox['max'] = count($result); + } + + // Process 50 entities at a time. + $limit = 50; + $nids = array_slice($result, $sandbox['current'], $limit); + $nodes = Node::loadMultiple($nids); + $languageManager = \Drupal::languageManager(); + foreach ($nodes as $node) { + // Set current languages to match node's language for proper + // token replacement. + $languageManager->setCurrentLanguage($node->language(), LanguageInterface::TYPE_CONTENT); + $languageManager->setCurrentLanguage($node->language(), LanguageInterface::TYPE_URL); + + // Update Entity URL alias. + $path_value = $node->get('path')->getValue(); + $path_value = $path_value ?: [[]]; + $path_value[0]['pathauto'] = PathautoState::CREATE; + $node->set('path', $path_value); + \Drupal::service('pathauto.generator')->updateEntityAlias($node, 'bulkupdate', ['message' => TRUE]); + + // Update our progress information. + $sandbox['current']++; + } + + \Drupal::messenger()->addMessage($sandbox['current'] . ' items processed.'); + + $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['current'] / $sandbox['max']); + + if ($sandbox['#finished'] >= 1) { + \Drupal::messenger()->addMessage('The batch URL Alias update is finished.'); + } +} diff --git a/public/modules/custom/helfi_group/helfi_group.tokens.inc b/public/modules/custom/helfi_group/helfi_group.tokens.inc index 4afd799e..1d3885be 100644 --- a/public/modules/custom/helfi_group/helfi_group.tokens.inc +++ b/public/modules/custom/helfi_group/helfi_group.tokens.inc @@ -78,10 +78,19 @@ function helfi_group_tokens($type, $tokens, array $data, array $options, Bubblea break; } + // Resolve the path to its alias so the token works in both normal + // requests and during update hooks. In update hooks the path_alias + // outbound processor is disabled (see UpdateServiceProvider), so + // toUrl('canonical')->toString() would return the internal path + // (e.g. node/123). Using the alias manager directly returns the + // alias in both contexts. + $internal_path = $newsParent->toUrl('canonical')->getInternalPath(); + $path = \Drupal::service('path_alias.manager')->getAliasByPath("/$internal_path", $langcode); + // Set the token replacement using the news parent path, but filtering // out langcode and site prefix sections. $replacements[$original] = _helfi_group_filter_path( - $newsParent->toUrl('canonical')->toString(), + $path, [ $langcode, _helfi_group_get_site_prefix($langcode), From 4cd5c86d6122a905bbf35d21c08b408e1627a769 Mon Sep 17 00:00:00 2001 From: Toni Sinisalo Date: Tue, 17 Feb 2026 17:01:29 +0200 Subject: [PATCH 5/9] UHF-12760: Adding tests. --- .../Menu/GroupMenuActiveTrailProxyTest.php | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailProxyTest.php diff --git a/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailProxyTest.php b/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailProxyTest.php new file mode 100644 index 00000000..66436c0c --- /dev/null +++ b/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailProxyTest.php @@ -0,0 +1,86 @@ +prophesize(MenuLinkInterface::class); + $activeLink->getTitle()->willReturn('Delegated link'); + + $realService = $this->prophesize(GroupMenuActiveTrail::class); + $realService->getActiveLink('test-menu')->willReturn($activeLink->reveal()); + + $container = $this->prophesize(ContainerInterface::class); + $container->get(self::SERVICE_ID)->willReturn($realService->reveal()); + + $proxy = new GroupMenuActiveTrailProxy($container->reveal(), self::SERVICE_ID); + + $result = $proxy->getActiveLink('test-menu'); + + $this->assertSame($activeLink->reveal(), $result); + $this->assertEquals('Delegated link', $result->getTitle()); + $realService->getActiveLink('test-menu')->shouldHaveBeenCalledOnce(); + $container->get(self::SERVICE_ID)->shouldHaveBeenCalledOnce(); + } + + /** + * Tests that the proxy delegates getActiveTrailIds() to the real service. + */ + public function testGetActiveTrailIdsDelegatesToRealService(): void { + $trailIds = ['menu_link_content:abc', 'menu_link_content:def']; + + $realService = $this->prophesize(GroupMenuActiveTrail::class); + $realService->getActiveTrailIds('group_menu_link_content-1')->willReturn($trailIds); + + $container = $this->prophesize(ContainerInterface::class); + $container->get(self::SERVICE_ID)->willReturn($realService->reveal()); + + $proxy = new GroupMenuActiveTrailProxy($container->reveal(), self::SERVICE_ID); + + $result = $proxy->getActiveTrailIds('group_menu_link_content-1'); + + $this->assertSame($trailIds, $result); + $realService->getActiveTrailIds('group_menu_link_content-1')->shouldHaveBeenCalledOnce(); + } + + /** + * Tests that the real service is only loaded once. + */ + public function testServiceIsLazyLoadedOnce(): void { + $realService = $this->prophesize(GroupMenuActiveTrail::class); + $realService->getActiveLink(Argument::any())->willReturn(NULL); + $realService->getActiveTrailIds(Argument::any())->willReturn([]); + + $container = $this->prophesize(ContainerInterface::class); + $container->get(self::SERVICE_ID)->willReturn($realService->reveal()); + + $proxy = new GroupMenuActiveTrailProxy($container->reveal(), self::SERVICE_ID); + + $proxy->getActiveLink('menu'); + $proxy->getActiveTrailIds('menu'); + + $container->get(self::SERVICE_ID)->shouldHaveBeenCalledTimes(1); + } + +} From e164ba3f3235d44a32124964c7ad8bc55d810777 Mon Sep 17 00:00:00 2001 From: Toni Sinisalo Date: Thu, 19 Feb 2026 12:39:52 +0200 Subject: [PATCH 6/9] UHF-12760: Making update hook messaing a bit more sensible and updating tests. --- .../custom/helfi_group/helfi_group.install | 4 +- .../Menu/GroupMenuActiveTrailProxyTest.php | 86 +++++++++---------- 2 files changed, 41 insertions(+), 49 deletions(-) diff --git a/public/modules/custom/helfi_group/helfi_group.install b/public/modules/custom/helfi_group/helfi_group.install index 1a2712b5..dc85fd32 100644 --- a/public/modules/custom/helfi_group/helfi_group.install +++ b/public/modules/custom/helfi_group/helfi_group.install @@ -98,11 +98,9 @@ function helfi_group_update_9002(&$sandbox): void { $sandbox['current']++; } - \Drupal::messenger()->addMessage($sandbox['current'] . ' items processed.'); - $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['current'] / $sandbox['max']); if ($sandbox['#finished'] >= 1) { - \Drupal::messenger()->addMessage('The batch URL Alias update is finished.'); + \Drupal::messenger()->addMessage('Url alias updated for ' . $sandbox['current'] . ' news items.'); } } diff --git a/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailProxyTest.php b/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailProxyTest.php index 66436c0c..5b7df815 100644 --- a/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailProxyTest.php +++ b/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailProxyTest.php @@ -8,7 +8,6 @@ use Drupal\Core\Menu\MenuLinkInterface; use Drupal\helfi_group\Menu\GroupMenuActiveTrail; use Drupal\Tests\UnitTestCase; -use Prophecy\Argument; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -22,64 +21,59 @@ class GroupMenuActiveTrailProxyTest extends UnitTestCase { private const SERVICE_ID = 'helfi_group.group_menu_active_trail'; /** - * Tests that the proxy delegates getActiveLink() to the real service. + * Tests all proxy methods delegate to the real service. */ - public function testGetActiveLinkDelegatesToRealService(): void { + public function testProxyDelegatesAllMethodsAndLoadsServiceOnce(): void { $activeLink = $this->prophesize(MenuLinkInterface::class); $activeLink->getTitle()->willReturn('Delegated link'); $realService = $this->prophesize(GroupMenuActiveTrail::class); $realService->getActiveLink('test-menu')->willReturn($activeLink->reveal()); + $realService->getActiveTrailIds('group_menu_link_content-1')->willReturn([ + 'menu_link_content:abc', + 'menu_link_content:def', + ]); + $realService->has('cache_key')->willReturn(TRUE); + $realService->get('cache_key')->willReturn('cached_value'); + $realService->set('key', 'value')->willReturn($realService->reveal()); + $realService->delete('key')->willReturn(TRUE); + $realService->reset()->willReturn(NULL); + $realService->clear()->willReturn(NULL); + $realService->destruct()->willReturn(NULL); $container = $this->prophesize(ContainerInterface::class); $container->get(self::SERVICE_ID)->willReturn($realService->reveal()); $proxy = new GroupMenuActiveTrailProxy($container->reveal(), self::SERVICE_ID); - $result = $proxy->getActiveLink('test-menu'); - - $this->assertSame($activeLink->reveal(), $result); - $this->assertEquals('Delegated link', $result->getTitle()); - $realService->getActiveLink('test-menu')->shouldHaveBeenCalledOnce(); - $container->get(self::SERVICE_ID)->shouldHaveBeenCalledOnce(); - } - - /** - * Tests that the proxy delegates getActiveTrailIds() to the real service. - */ - public function testGetActiveTrailIdsDelegatesToRealService(): void { - $trailIds = ['menu_link_content:abc', 'menu_link_content:def']; - - $realService = $this->prophesize(GroupMenuActiveTrail::class); - $realService->getActiveTrailIds('group_menu_link_content-1')->willReturn($trailIds); - - $container = $this->prophesize(ContainerInterface::class); - $container->get(self::SERVICE_ID)->willReturn($realService->reveal()); - - $proxy = new GroupMenuActiveTrailProxy($container->reveal(), self::SERVICE_ID); - - $result = $proxy->getActiveTrailIds('group_menu_link_content-1'); - - $this->assertSame($trailIds, $result); + // Call every proxy method. + $this->assertSame($activeLink->reveal(), $proxy->getActiveLink('test-menu')); + $this->assertEquals('Delegated link', $proxy->getActiveLink('test-menu')->getTitle()); + $this->assertSame([ + 'menu_link_content:abc', + 'menu_link_content:def', + ], $proxy->getActiveTrailIds('group_menu_link_content-1')); + $this->assertTrue($proxy->has('cache_key')); + $this->assertSame('cached_value', $proxy->get('cache_key')); + $proxy->set('key', 'value'); + $proxy->delete('key'); + $proxy->reset(); + $proxy->clear(); + $proxy->destruct(); + + // Every proxy method must have called the corresponding real service + // method. + $realService->getActiveLink('test-menu')->shouldHaveBeenCalledTimes(2); $realService->getActiveTrailIds('group_menu_link_content-1')->shouldHaveBeenCalledOnce(); - } - - /** - * Tests that the real service is only loaded once. - */ - public function testServiceIsLazyLoadedOnce(): void { - $realService = $this->prophesize(GroupMenuActiveTrail::class); - $realService->getActiveLink(Argument::any())->willReturn(NULL); - $realService->getActiveTrailIds(Argument::any())->willReturn([]); - - $container = $this->prophesize(ContainerInterface::class); - $container->get(self::SERVICE_ID)->willReturn($realService->reveal()); - - $proxy = new GroupMenuActiveTrailProxy($container->reveal(), self::SERVICE_ID); - - $proxy->getActiveLink('menu'); - $proxy->getActiveTrailIds('menu'); - + $realService->has('cache_key')->shouldHaveBeenCalledOnce(); + $realService->get('cache_key')->shouldHaveBeenCalledOnce(); + $realService->set('key', 'value')->shouldHaveBeenCalledOnce(); + $realService->delete('key')->shouldHaveBeenCalledOnce(); + $realService->reset()->shouldHaveBeenCalledOnce(); + $realService->clear()->shouldHaveBeenCalledOnce(); + $realService->destruct()->shouldHaveBeenCalledOnce(); + + // Original service must be loaded from the container only once. $container->get(self::SERVICE_ID)->shouldHaveBeenCalledTimes(1); } From 6baf89d2053e9f52d65483b86ed02578a8388075 Mon Sep 17 00:00:00 2001 From: Toni Sinisalo Date: Mon, 23 Feb 2026 14:07:20 +0200 Subject: [PATCH 7/9] UHF-12760: Overriding the group menu block instead of the active trail service. --- .../src/HelfiGroupServiceProvider.php | 29 -- .../helfi_group/src/Hook/BlockAlter.php | 28 ++ .../src/Menu/GroupMenuActiveTrail.php | 110 ----- .../src/Plugin/Block/HelfiGroupMenuBlock.php | 213 +++++++++ .../ProxyClass/Menu/GroupMenuActiveTrail.php | 144 ------ .../Kernel/HelfiGroupServiceProviderTest.php | 35 -- .../Plugin/Block/HelfiGroupMenuBlockTest.php | 428 ++++++++++++++++++ .../Menu/GroupMenuActiveTrailProxyTest.php | 80 ---- .../Unit/Menu/GroupMenuActiveTrailTest.php | 204 --------- 9 files changed, 669 insertions(+), 602 deletions(-) delete mode 100644 public/modules/custom/helfi_group/src/HelfiGroupServiceProvider.php create mode 100644 public/modules/custom/helfi_group/src/Hook/BlockAlter.php delete mode 100644 public/modules/custom/helfi_group/src/Menu/GroupMenuActiveTrail.php create mode 100644 public/modules/custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php delete mode 100644 public/modules/custom/helfi_group/src/ProxyClass/Menu/GroupMenuActiveTrail.php delete mode 100644 public/modules/custom/helfi_group/tests/src/Kernel/HelfiGroupServiceProviderTest.php create mode 100644 public/modules/custom/helfi_group/tests/src/Kernel/Plugin/Block/HelfiGroupMenuBlockTest.php delete mode 100644 public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailProxyTest.php delete mode 100644 public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailTest.php diff --git a/public/modules/custom/helfi_group/src/HelfiGroupServiceProvider.php b/public/modules/custom/helfi_group/src/HelfiGroupServiceProvider.php deleted file mode 100644 index 460efe52..00000000 --- a/public/modules/custom/helfi_group/src/HelfiGroupServiceProvider.php +++ /dev/null @@ -1,29 +0,0 @@ -hasDefinition('menu.active_trail')) { - $definition = $container->getDefinition('menu.active_trail'); - $definition->setClass(GroupMenuActiveTrail::class); - $definition - ->addArgument(new Reference('group.group_route_context')); - } - } - -} diff --git a/public/modules/custom/helfi_group/src/Hook/BlockAlter.php b/public/modules/custom/helfi_group/src/Hook/BlockAlter.php new file mode 100644 index 00000000..060c07ac --- /dev/null +++ b/public/modules/custom/helfi_group/src/Hook/BlockAlter.php @@ -0,0 +1,28 @@ +groupRouteContext = $group_route_context; - } - - /** - * {@inheritdoc} - * - * Handles active trail for group news items, which are not included in - * the menu tree. Instead, the active trail is built from the news parent - * node. - */ - public function getActiveLink($menu_name = NULL) { - // If the menu name is not a group menu, use the default implementation. - if (!str_starts_with($menu_name, 'group_menu_link_content-')) { - return parent::getActiveLink($menu_name); - } - - // If this is not a news item, use the default implementation. - $node = $this->routeMatch->getParameter('node'); - if (!$node || !($node instanceof NodeInterface) || $node->bundle() !== 'news_item') { - return parent::getActiveLink($menu_name); - } - - // If no group is found, use the default implementation. - $group = $this->groupRouteContext->getBestCandidate(); - if (!$group) { - return parent::getActiveLink($menu_name); - } - - // If no news parent is found, use the default implementation. - $newsParent = NULL; - if ($group->hasField('field_group_news_parent')) { - $newsParent = $group->get('field_group_news_parent')->entity; - } - if (!$newsParent || !($newsParent instanceof NodeInterface)) { - return parent::getActiveLink($menu_name); - } - - // The menu links coming from the storage are already sorted by depth, - // weight and ID. - $found = NULL; - $links = []; - - $route_name = $newsParent->toUrl()->getRouteName(); - if ($route_name) { - $route_parameters = $newsParent->toUrl()->getRouteParameters(); - - // Load links matching this route. - $links = $this->menuLinkManager->loadLinksByRoute($route_name, $route_parameters, $menu_name); - } - - // Select the first matching link. - if ($links) { - $found = reset($links); - } - return $found; - } - -} diff --git a/public/modules/custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php b/public/modules/custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php new file mode 100644 index 00000000..3cf708d6 --- /dev/null +++ b/public/modules/custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php @@ -0,0 +1,213 @@ +routeMatch = $route_match; + $this->menuLinkManager = $menu_link_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('menu.link_tree'), + $container->get('menu.active_trail'), + $container->get('entity_type.manager'), + $container->get('current_route_match'), + $container->get('plugin.manager.menu.link'), + ); + } + + /** + * {@inheritdoc} + */ + public function build() { + $menu_name = $this->getMenuName(); + // If unable to determine the menu, prevent the block from rendering. + if (!$menu_name = $this->getMenuName()) { + return []; + } + + // Get block configuration and menu tree parameters. + $level = $this->configuration['level']; + $depth = $this->configuration['depth']; + $relative_visibility = $this->configuration['relative_visibility'] ?? FALSE; + $parameters = $this->menuTree->getCurrentRouteMenuTreeParameters($menu_name); + + // If active trail is empty, we might be on a group news item, that + // is not included in the menu tree. This fallback handling for empty + // active trail is the only abstraction from the parent class build method. + if (empty(array_filter($parameters->activeTrail))) { + $parameters->setActiveTrail($this->getActiveTrailForNewsItem($menu_name)); + } + + // Adjust the menu tree parameters based on the block's configuration. + $parameters->setMinDepth($level); + + // Adjust menu root in cases where the active menu item is below root and + // only a subset of the full menu is to be shown, or possibly not at all. + $relative_level = count($parameters->activeTrail); + if ($level > 1 || ($relative_visibility && $relative_level > $level)) { + $menu_trail_ids = array_reverse(array_values($parameters->activeTrail)); + // For relative visibility we reset the root relative to the active + // menu item. + if ($relative_visibility) { + $menu_root = $menu_trail_ids[$relative_level - 2]; + $parameters->setRoot($menu_root); + } + // For absolute visibility, we reset root relative to the original and + // adjust the minimum depth. + elseif ($relative_level >= $level) { + $menu_root = $menu_trail_ids[$level - 1]; + $parameters->setRoot($menu_root)->setMinDepth(1); + } + // If the active menu item is not at or above the visibility level, and + // relative visibility is not in play, then do not show the menu. + else { + return []; + } + } + + // When the depth is configured to zero, there is no depth limit. When depth + // is non-zero, it indicates the number of levels that must be displayed. + // Hence, this is a relative depth that we must convert to an actual + // (absolute) depth, that may never exceed the maximum depth. + if ($depth > 0) { + $relative_depth = $relative_visibility ? $level + $depth - 1 : $depth; + $parameters->setMaxDepth(\min($relative_depth, $this->menuTree->maxDepth())); + } + + // If expandedParents is empty, the whole menu tree is built. + if ($this->configuration['expand_all_items']) { + $parameters->expandedParents = []; + } + + $tree = $this->menuTree->load($menu_name, $parameters); + $tree = $this->menuTree->transform($tree, $this->getMenuManipulators()); + $build = $this->menuTree->build($tree); + $menu_instance = $this->getMenuInstance(); + $build['#group_content_menu'] = [ + 'menu_name' => $menu_name, + 'theme_hook_suggestion' => $this->configuration['theme_hook_suggestion'], + ]; + if ($menu_instance instanceof GroupContentMenuInterface) { + $build['#group_content_menu']['group_content_menu_type'] = $menu_instance->bundle(); + $build['#contextual_links']['group_menu'] = [ + 'route_parameters' => [ + 'group' => $this->getContext('group')->getContextData()->getValue()->id(), + 'group_content_menu' => $menu_instance->id(), + ], + ]; + + } + $build['#theme'] = 'menu'; + return $build; + } + + /** + * Get the active trail for a news item node. + * + * @param string $menu_name + * The menu name. + * + * @return array + * The active trail. + */ + private function getActiveTrailForNewsItem(string $menu_name): array { + $active_trail = ['' => '']; + + // Fetch current node and only continue if it's a news_item. + $node = $this->routeMatch->getParameter('node'); + if (!$node || !($node instanceof NodeInterface) || $node->bundle() !== 'news_item') { + return $active_trail; + } + + // Fetch current group from plugin context. + $group = $this->getContextValue('group'); + if (!$group || !($group instanceof GroupInterface)) { + return $active_trail; + } + + // Fetch news parent from group. + $newsParent = NULL; + if ($group->hasField('field_group_news_parent')) { + $newsParent = $group->get('field_group_news_parent')->entity; + } + if (!$newsParent || !($newsParent instanceof NodeInterface)) { + return $active_trail; + } + + // Build active trail from news parent. + $found = NULL; + $links = []; + $route_name = $newsParent->toUrl()->getRouteName(); + if ($route_name) { + $route_parameters = $newsParent->toUrl()->getRouteParameters(); + + // Load links matching this route. + $links = $this->menuLinkManager->loadLinksByRoute($route_name, $route_parameters, $menu_name); + } + if ($links) { + $found = reset($links); + } + if ($found) { + if ($parents = $this->menuLinkManager->getParentIds($found->getPluginId())) { + $active_trail = $parents + $active_trail; + } + } + + return $active_trail; + } + +} diff --git a/public/modules/custom/helfi_group/src/ProxyClass/Menu/GroupMenuActiveTrail.php b/public/modules/custom/helfi_group/src/ProxyClass/Menu/GroupMenuActiveTrail.php deleted file mode 100644 index ee1de15b..00000000 --- a/public/modules/custom/helfi_group/src/ProxyClass/Menu/GroupMenuActiveTrail.php +++ /dev/null @@ -1,144 +0,0 @@ -container = $container; - $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id; - } - - /** - * Lazy loads the real service from the container. - * - * @return object - * Returns the constructed real service. - */ - protected function lazyLoadItself() - { - if (!isset($this->service)) { - $this->service = $this->container->get($this->drupalProxyOriginalServiceId); - } - - return $this->service; - } - - /** - * {@inheritdoc} - */ - public function getActiveLink($menu_name = NULL) - { - return $this->lazyLoadItself()->getActiveLink($menu_name); - } - - /** - * {@inheritdoc} - */ - public function getActiveTrailIds($menu_name) - { - return $this->lazyLoadItself()->getActiveTrailIds($menu_name); - } - - /** - * {@inheritdoc} - */ - public function has($key) - { - return $this->lazyLoadItself()->has($key); - } - - /** - * {@inheritdoc} - */ - public function get($key) - { - return $this->lazyLoadItself()->get($key); - } - - /** - * {@inheritdoc} - */ - public function set($key, $value) - { - return $this->lazyLoadItself()->set($key, $value); - } - - /** - * {@inheritdoc} - */ - public function delete($key) - { - return $this->lazyLoadItself()->delete($key); - } - - /** - * {@inheritdoc} - */ - public function reset() - { - return $this->lazyLoadItself()->reset(); - } - - /** - * {@inheritdoc} - */ - public function clear() - { - return $this->lazyLoadItself()->clear(); - } - - /** - * {@inheritdoc} - */ - public function destruct() - { - return $this->lazyLoadItself()->destruct(); - } - - } - -} diff --git a/public/modules/custom/helfi_group/tests/src/Kernel/HelfiGroupServiceProviderTest.php b/public/modules/custom/helfi_group/tests/src/Kernel/HelfiGroupServiceProviderTest.php deleted file mode 100644 index 9ce6b9aa..00000000 --- a/public/modules/custom/helfi_group/tests/src/Kernel/HelfiGroupServiceProviderTest.php +++ /dev/null @@ -1,35 +0,0 @@ -alter($this->container); - $definition = $this->container->getDefinition('menu.active_trail'); - $this->assertEquals(GroupMenuActiveTrail::class, $definition->getClass()); - } - -} diff --git a/public/modules/custom/helfi_group/tests/src/Kernel/Plugin/Block/HelfiGroupMenuBlockTest.php b/public/modules/custom/helfi_group/tests/src/Kernel/Plugin/Block/HelfiGroupMenuBlockTest.php new file mode 100644 index 00000000..16201d06 --- /dev/null +++ b/public/modules/custom/helfi_group/tests/src/Kernel/Plugin/Block/HelfiGroupMenuBlockTest.php @@ -0,0 +1,428 @@ + 2, + 'depth' => 0, + 'expand_all_items' => FALSE, + 'relative_visibility' => FALSE, + 'theme_hook_suggestion' => '', + ]; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'block', + 'system', + 'field', + 'user', + 'node', + 'menu_link_content', + 'link', + 'flexible_permissions', + 'group', + 'group_content_menu', + 'helfi_group', + 'helfi_tpr', + 'language', + 'locale', + 'menu_block_current_language', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->installConfig(['system']); + $this->installEntitySchema('group'); + $this->installEntitySchema('group_relationship'); + $this->installConfig(['group']); + $this->installEntitySchema('group_content_menu'); + $this->installEntitySchema('menu_link_content'); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + + // Set current user so Group::postSave() can add the creator as a member. + $this->setCurrentUser($this->createUser()); + + // Create the group content menu type that BlockAlter replaces with + // HelfiGroupMenuBlock. + GroupContentMenuType::create([ + 'id' => 'kasko_group_menu', + 'label' => 'Kasko group menu', + ])->save(); + + // Create node types for getActiveTrailForNewsItem tests. + NodeType::create(['type' => 'news_item', 'name' => 'News item'])->save(); + NodeType::create(['type' => 'page', 'name' => 'Page'])->save(); + + // Clear block plugin cache so the derivative and block_alter are applied. + $this->container->get('plugin.manager.block')->clearCachedDefinitions(); + } + + /** + * Tests that build() returns empty when group context is missing. + */ + public function testBuildWithoutGroupContext(): void { + $block = $this->createBlock(); + + $this->assertSame([], $block->build()); + } + + /** + * Tests that build() returns empty when group has no menu. + */ + public function testBuildWithGroupWithoutMenu(): void { + $group_type = $this->createGroupType(); + $group = $this->createGroup(['type' => $group_type->id()]); + + $block = $this->createBlock($group); + + $this->assertSame([], $block->build()); + } + + /** + * Tests that build() returns menu render array when group has a menu. + * + * With level 2 the block requires a non-empty active trail. We add a menu + * link and simulate being on that page so the trail is set and the menu + * builds. + */ + public function testBuildWithGroupAndMenu(): void { + [$group, $menu_name, $menu] = $this->setUpGroupWithMenu(); + $page = Node::create(['type' => 'page', 'title' => 'Group page']); + $page->save(); + MenuLinkContent::create([ + 'title' => 'Group page', + 'link' => ['uri' => 'entity:node/' . $page->id()], + 'menu_name' => $menu_name, + ])->save(); + $this->pushRequestWithNode($page); + + $block = $this->createBlock($group); + $build = $block->build(); + + $this->assertNotEmpty($build); + $this->assertSame('menu', $build['#theme']); + $this->assertSame($menu_name, $build['#group_content_menu']['menu_name']); + $this->assertSame('kasko_group_menu', $build['#group_content_menu']['group_content_menu_type']); + $this->assertArrayHasKey('group_menu', $build['#contextual_links']); + $this->assertSame($group->id(), $build['#contextual_links']['group_menu']['route_parameters']['group']); + $this->assertSame($menu->id(), $build['#contextual_links']['group_menu']['route_parameters']['group_content_menu']); + } + + /** + * Tests that the block uses HelfiGroupMenuBlock class (via block_alter). + */ + public function testBlockUsesHelfiGroupMenuBlockClass(): void { + $block = $this->createBlock(); + + $this->assertInstanceOf(HelfiGroupMenuBlock::class, $block); + } + + /** + * Tests getActiveTrailForNewsItem: empty trail when route has no node. + * + * With level 2 and no node in route, active trail stays ['' => ''], so + * relative_level < level and build() returns [] (no fallback applies). + */ + public function testBuildWithEmptyTrailAndNoNodeInRoute(): void { + [$group] = $this->setUpGroupWithMenu(); + $this->pushRequestWithRoute('user.login', '/user/login', []); + + $build = $this->createBlock($group)->build(); + + $this->assertSame([], $build); + } + + /** + * Tests getActiveTrailForNewsItem: empty trail when node is not news_item. + * + * Fallback only runs for news_item; for other bundles it returns ['' => '']. + * With level 2, relative_level stays 1 so build() returns []. + */ + public function testBuildWithEmptyTrailAndNonNewsItemNode(): void { + [$group] = $this->setUpGroupWithMenu(); + $page_node = Node::create(['type' => 'page', 'title' => 'A page']); + $page_node->save(); + $this->pushRequestWithNode($page_node); + + $build = $this->createBlock($group)->build(); + + $this->assertSame([], $build); + } + + /** + * Tests getActiveTrailForNewsItem: empty trail when group context is missing. + * + * Without group context the block has no menu name and returns [] early. + */ + public function testBuildWithEmptyTrailAndNewsItemWithoutGroupContext(): void { + $this->setUpGroupWithMenu(); + $news_item = Node::create(['type' => 'news_item', 'title' => 'News']); + $news_item->save(); + $this->pushRequestWithNode($news_item); + + $this->assertSame([], $this->createBlock()->build()); + } + + /** + * Empty trail when group has no news parent field. + * + * Tests getActiveTrailForNewsItem fallback. Fallback returns ['' => ''] + * when group has no field_group_news_parent. With level 2, build() + * returns []. + */ + public function testBuildWithEmptyTrailAndNewsItemGroupWithoutNewsParentField(): void { + [$group] = $this->setUpGroupWithMenu(); + $news_item = Node::create(['type' => 'news_item', 'title' => 'News']); + $news_item->save(); + $this->pushRequestWithNode($news_item); + + $build = $this->createBlock($group)->build(); + + $this->assertSame([], $build); + } + + /** + * Tests getActiveTrailForNewsItem: trail from news parent when in menu. + * + * With level 2, an empty trail would cause build() to return []. The fallback + * resolves the news parent link in the menu and sets the active trail, so + * relative_level >= level and the menu is rendered. + */ + public function testBuildWithEmptyTrailAndNewsItemWithNewsParentInMenu(): void { + [$group, $menu_name] = $this->setUpGroupWithMenuAndNewsParent(); + $news_item = Node::create(['type' => 'news_item', 'title' => 'A news item']); + $news_item->save(); + $this->pushRequestWithNode($news_item); + + $build = $this->createBlock($group)->build(); + + $this->assertNotEmpty($build); + $this->assertSame('menu', $build['#theme']); + $this->assertArrayHasKey('#group_content_menu', $build); + $this->assertSame($menu_name, $build['#group_content_menu']['menu_name']); + } + + /** + * Sets up a group with a kasko group menu; returns group, menu name, menu. + * + * @return array{0: \Drupal\group\Entity\GroupInterface, 1: string, + * 2: \Drupal\group_content_menu\Entity\GroupContentMenu} + * Group, menu name and menu entity. + */ + private function setUpGroupWithMenu(): array { + $group_type = $this->createGroupType(); + $relationship_type_storage = $this->getRelationshipTypeStorage(); + $relationship_type_storage->createFromPlugin($group_type, self::BLOCK_PLUGIN_ID)->save(); + + $group = $this->createGroup(['type' => $group_type->id()]); + $menu = GroupContentMenu::create(['label' => 'Test menu', 'bundle' => 'kasko_group_menu']); + $menu->save(); + + $this->attachMenuToGroup($group_type, $group, $menu); + + $menu_name = GroupContentMenuInterface::MENU_PREFIX . $menu->id(); + Menu::create(['id' => $menu_name, 'label' => 'Test', 'description' => ''])->save(); + + return [$group, $menu_name, $menu]; + } + + /** + * Sets up a group with menu and a news parent node (field + menu link). + * + * @return array{0: \Drupal\group\Entity\GroupInterface, 1: string} + * Group and menu name. + */ + private function setUpGroupWithMenuAndNewsParent(): array { + $group_type = $this->createGroupType(); + + FieldStorageConfig::create([ + 'field_name' => 'field_group_news_parent', + 'entity_type' => 'group', + 'type' => 'entity_reference', + 'cardinality' => 1, + 'settings' => ['target_type' => 'node'], + ])->save(); + FieldConfig::create([ + 'field_name' => 'field_group_news_parent', + 'entity_type' => 'group', + 'bundle' => $group_type->id(), + 'label' => 'News parent', + 'settings' => ['handler_settings' => ['target_bundles' => ['page' => 'page']]], + ])->save(); + + $news_parent = Node::create(['type' => 'page', 'title' => 'News parent page']); + $news_parent->save(); + + $relationship_type_storage = $this->getRelationshipTypeStorage(); + $relationship_type_storage->createFromPlugin($group_type, self::BLOCK_PLUGIN_ID)->save(); + + $group = $this->createGroup(['type' => $group_type->id()]); + $group->set('field_group_news_parent', $news_parent->id()); + $group->save(); + + $menu = GroupContentMenu::create(['label' => 'Test menu', 'bundle' => 'kasko_group_menu']); + $menu->save(); + $this->attachMenuToGroup($group_type, $group, $menu); + + $menu_name = GroupContentMenuInterface::MENU_PREFIX . $menu->id(); + Menu::create(['id' => $menu_name, 'label' => 'Test', 'description' => ''])->save(); + + MenuLinkContent::create([ + 'title' => 'News parent', + 'link' => ['uri' => 'entity:node/' . $news_parent->id()], + 'menu_name' => $menu_name, + ])->save(); + + return [$group, $menu_name]; + } + + /** + * Creates the block plugin. + * + * Optionally with group context and configuration overrides. + * + * @param \Drupal\group\Entity\GroupInterface|null $group + * The group to set as context, or NULL for no context. + * @param array $configuration + * Configuration overrides (merged with block defaults). Use for level/depth + * when testing getActiveTrailForNewsItem behavior. + * + * @return \Drupal\helfi_group\Plugin\Block\HelfiGroupMenuBlock + * The block plugin instance. + */ + private function createBlock( + ?GroupInterface $group = NULL, + array $configuration = [], + ): HelfiGroupMenuBlock { + $block = $this->container->get('plugin.manager.block')->createInstance( + self::BLOCK_PLUGIN_ID, + $configuration + self::DEFAULT_BLOCK_CONFIG + ); + if ($group !== NULL) { + $block->setContextValue('group', $group); + } + return $block; + } + + /** + * Pushes a request with the given route and raw parameters. + */ + private function pushRequestWithRoute(string $route_name, string $path, array $raw_variables): void { + $route = $this->container->get('router.route_provider')->getRouteByName($route_name); + $request = Request::create($path); + $request->attributes->set('_route', $route_name); + $request->attributes->set('_route_object', $route); + $request->attributes->set('_raw_variables', new InputBag($raw_variables)); + $this->copySessionToRequest($request); + $this->container->get('request_stack')->push($request); + } + + /** + * Returns the group relationship type storage. + */ + private function getRelationshipTypeStorage(): GroupRelationshipTypeStorageInterface { + $storage = $this->entityTypeManager->getStorage('group_relationship_type'); + assert($storage instanceof GroupRelationshipTypeStorageInterface); + return $storage; + } + + /** + * Attaches the given group content menu to the group via a relationship. + */ + private function attachMenuToGroup(GroupTypeInterface $group_type, GroupInterface $group, GroupContentMenu $menu): void { + $relationship_type_id = $this->getRelationshipTypeStorage()->getRelationshipTypeId($group_type->id(), self::BLOCK_PLUGIN_ID); + $this->entityTypeManager->getStorage('group_relationship')->create([ + 'type' => $relationship_type_id, + 'gid' => $group->id(), + 'label' => 'Test menu', + 'entity_id' => $menu, + ])->save(); + } + + /** + * Pushes a request onto the stack that has the given node as route parameter. + * + * Uses the real entity.node.canonical route and raw parameters so URL + * generation (e.g. in PathMatcher) works. Copies session so tearDown does + * not fail. + * + * @param \Drupal\node\Entity\Node $node + * The node to set as 'node' parameter. + */ + private function pushRequestWithNode(Node $node): void { + $route = $this->container->get('router.route_provider')->getRouteByName('entity.node.canonical'); + $request = Request::create('/node/' . $node->id()); + $request->attributes->set('_route', 'entity.node.canonical'); + $request->attributes->set('_route_object', $route); + $request->attributes->set('node', $node); + $request->attributes->set('_raw_variables', new InputBag(['node' => $node->id()])); + $this->copySessionToRequest($request); + $this->container->get('request_stack')->push($request); + } + + /** + * Copies the current request's session onto the given request. + * + * Prevents SessionNotFoundException in tearDown when tests push a new + * request. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request to attach the session to. + */ + private function copySessionToRequest(Request $request): void { + $current = $this->container->get('request_stack')->getCurrentRequest(); + if ($current && $current->hasSession()) { + $request->setSession($current->getSession()); + } + } + +} diff --git a/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailProxyTest.php b/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailProxyTest.php deleted file mode 100644 index 5b7df815..00000000 --- a/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailProxyTest.php +++ /dev/null @@ -1,80 +0,0 @@ -prophesize(MenuLinkInterface::class); - $activeLink->getTitle()->willReturn('Delegated link'); - - $realService = $this->prophesize(GroupMenuActiveTrail::class); - $realService->getActiveLink('test-menu')->willReturn($activeLink->reveal()); - $realService->getActiveTrailIds('group_menu_link_content-1')->willReturn([ - 'menu_link_content:abc', - 'menu_link_content:def', - ]); - $realService->has('cache_key')->willReturn(TRUE); - $realService->get('cache_key')->willReturn('cached_value'); - $realService->set('key', 'value')->willReturn($realService->reveal()); - $realService->delete('key')->willReturn(TRUE); - $realService->reset()->willReturn(NULL); - $realService->clear()->willReturn(NULL); - $realService->destruct()->willReturn(NULL); - - $container = $this->prophesize(ContainerInterface::class); - $container->get(self::SERVICE_ID)->willReturn($realService->reveal()); - - $proxy = new GroupMenuActiveTrailProxy($container->reveal(), self::SERVICE_ID); - - // Call every proxy method. - $this->assertSame($activeLink->reveal(), $proxy->getActiveLink('test-menu')); - $this->assertEquals('Delegated link', $proxy->getActiveLink('test-menu')->getTitle()); - $this->assertSame([ - 'menu_link_content:abc', - 'menu_link_content:def', - ], $proxy->getActiveTrailIds('group_menu_link_content-1')); - $this->assertTrue($proxy->has('cache_key')); - $this->assertSame('cached_value', $proxy->get('cache_key')); - $proxy->set('key', 'value'); - $proxy->delete('key'); - $proxy->reset(); - $proxy->clear(); - $proxy->destruct(); - - // Every proxy method must have called the corresponding real service - // method. - $realService->getActiveLink('test-menu')->shouldHaveBeenCalledTimes(2); - $realService->getActiveTrailIds('group_menu_link_content-1')->shouldHaveBeenCalledOnce(); - $realService->has('cache_key')->shouldHaveBeenCalledOnce(); - $realService->get('cache_key')->shouldHaveBeenCalledOnce(); - $realService->set('key', 'value')->shouldHaveBeenCalledOnce(); - $realService->delete('key')->shouldHaveBeenCalledOnce(); - $realService->reset()->shouldHaveBeenCalledOnce(); - $realService->clear()->shouldHaveBeenCalledOnce(); - $realService->destruct()->shouldHaveBeenCalledOnce(); - - // Original service must be loaded from the container only once. - $container->get(self::SERVICE_ID)->shouldHaveBeenCalledTimes(1); - } - -} diff --git a/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailTest.php b/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailTest.php deleted file mode 100644 index f4e0f8b8..00000000 --- a/public/modules/custom/helfi_group/tests/src/Unit/Menu/GroupMenuActiveTrailTest.php +++ /dev/null @@ -1,204 +0,0 @@ - - */ - protected ObjectProphecy $menuLinkManager; - - /** - * The mocked route match. - * - * @var \Prophecy\Prophecy\ObjectProphecy - */ - protected ObjectProphecy $routeMatch; - - /** - * The mocked group route context. - * - * @var \Prophecy\Prophecy\ObjectProphecy - */ - protected ObjectProphecy $groupRouteContext; - - /** - * The mocked route match node. - * - * @var \Prophecy\Prophecy\ObjectProphecy - */ - protected ObjectProphecy $node; - - /** - * The mocked group. - * - * @var \Prophecy\Prophecy\ObjectProphecy - */ - protected ObjectProphecy $group; - - /** - * The mocked news parent node. - * - * @var \Prophecy\Prophecy\ObjectProphecy - */ - protected ObjectProphecy $newsParentNode; - - /** - * SUT instance. - * - * @var \Drupal\helfi_group\Menu\GroupMenuActiveTrail - */ - protected GroupMenuActiveTrail $groupMenuActiveTrail; - - /** - * {@inheritdoc} - */ - public function setUp(): void { - parent::setUp(); - - $testMenuLink = MenuLinkMock::createMock([ - 'id' => 'test_menu_link', - 'route_name' => 'test.route.name', - 'title' => 'Test Menu Link', - 'parent' => NULL, - ]); - - $groupTestMenuLink = MenuLinkMock::createMock([ - 'id' => 'group_test_menu_link', - 'route_name' => 'group_test.route.name', - 'title' => 'Group Test Menu Link', - 'parent' => NULL, - ]); - - $this->menuLinkManager = $this->prophesize(MenuLinkManagerInterface::class); - $this->menuLinkManager->loadLinksByRoute('test.route.name', Argument::any(), Argument::any())->willReturn([ - $testMenuLink, - ]); - $this->menuLinkManager->loadLinksByRoute('group_test.route.name', Argument::any(), Argument::any())->willReturn([ - $groupTestMenuLink, - ]); - - $parameterBag = $this->prophesize(ParameterBag::class); - $parameterBag->all()->willReturn([]); - - $this->node = $this->prophesize(NodeInterface::class); - - $url = $this->prophesize(Url::class); - $url->getRouteName()->willReturn('group_test.route.name'); - $url->getRouteParameters()->willReturn([]); - $this->newsParentNode = $this->prophesize(NodeInterface::class); - $this->newsParentNode->toUrl()->willReturn($url->reveal()); - - $newsParentField = (object) ['entity' => $this->newsParentNode->reveal()]; - - $this->group = $this->prophesize(GroupInterface::class); - $this->group->get('field_group_news_parent')->willReturn($newsParentField); - - $this->routeMatch = $this->prophesize(RouteMatchInterface::class); - $this->routeMatch->getRouteName()->willReturn('test.route.name'); - $this->routeMatch->getRawParameters()->willReturn($parameterBag->reveal()); - $this->routeMatch->getParameter('node')->willReturn($this->node->reveal()); - - $this->groupRouteContext = $this->prophesize(GroupRouteContext::class); - - $this->groupMenuActiveTrail = new GroupMenuActiveTrail( - $this->menuLinkManager->reveal(), - $this->routeMatch->reveal(), - $this->prophesize(CacheBackendInterface::class)->reveal(), - $this->prophesize(LockBackendInterface::class)->reveal(), - $this->prophesize(PathMatcherInterface::class)->reveal(), - $this->groupRouteContext->reveal(), - ); - } - - /** - * Data provider for the getActiveLink method. - * - * @return array - * Test data. - */ - public static function getActiveLinkDataProvider() { - $data = []; - - // Not a group menu. - $data['not a group menu'] = [ - 'menu_name' => 'not_a_group_menu', - ]; - - // Not a news item. - $data['not a news item'] = [ - 'node_bundle' => 'not_news_item', - ]; - - // No group found. - $data['no group found'] = [ - 'group_found' => FALSE, - ]; - - // No news parent found. - $data['no news parent found'] = [ - 'news_parent_found' => FALSE, - ]; - - // News parent found. - $data['news parent found'] = [ - 'expected_link_title' => 'Group Test Menu Link', - ]; - - return $data; - } - - /** - * Tests the getActiveLink method. - * - * @param string $menu_name - * The menu name. - * @param string $expected_link_title - * The expected link title. - * @param string $node_bundle - * The current route match node bundle. - * @param bool $group_found - * Whether the group was found. - * @param bool $news_parent_found - * Whether the news parent was found. - */ - #[DataProvider('getActiveLinkDataProvider')] - public function testGetActiveLink( - $menu_name = 'group_menu_link_content-1', - $expected_link_title = 'Test Menu Link', - $node_bundle = 'news_item', - $group_found = TRUE, - $news_parent_found = TRUE, - ) { - $this->node->bundle()->willReturn($node_bundle); - $this->groupRouteContext->getBestCandidate()->willReturn($group_found ? $this->group->reveal() : NULL); - $this->group->hasField('field_group_news_parent')->willReturn($news_parent_found); - $this->assertEquals($expected_link_title, $this->groupMenuActiveTrail->getActiveLink($menu_name)->getTitle()); - } - -} From 8804dcccc1bc51adfbdabbcc96f1b4f41fa51fd5 Mon Sep 17 00:00:00 2001 From: Toni Sinisalo Date: Mon, 23 Feb 2026 14:27:29 +0200 Subject: [PATCH 8/9] UHF-12760: Removing an unnecessary condition level. --- .../helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/public/modules/custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php b/public/modules/custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php index 3cf708d6..f661d8eb 100644 --- a/public/modules/custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php +++ b/public/modules/custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php @@ -201,10 +201,8 @@ private function getActiveTrailForNewsItem(string $menu_name): array { if ($links) { $found = reset($links); } - if ($found) { - if ($parents = $this->menuLinkManager->getParentIds($found->getPluginId())) { - $active_trail = $parents + $active_trail; - } + if ($found && $parents = $this->menuLinkManager->getParentIds($found->getPluginId())) { + $active_trail = $parents + $active_trail; } return $active_trail; From 60566cdf5d2eb1947f6e0aba128105bfc5f911b6 Mon Sep 17 00:00:00 2001 From: Toni Sinisalo Date: Mon, 23 Feb 2026 14:28:52 +0200 Subject: [PATCH 9/9] UHF-12760: Fixing a useless variable assignment. --- .../custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/modules/custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php b/public/modules/custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php index f661d8eb..7f9d04dc 100644 --- a/public/modules/custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php +++ b/public/modules/custom/helfi_group/src/Plugin/Block/HelfiGroupMenuBlock.php @@ -75,7 +75,7 @@ public static function create(ContainerInterface $container, array $configuratio public function build() { $menu_name = $this->getMenuName(); // If unable to determine the menu, prevent the block from rendering. - if (!$menu_name = $this->getMenuName()) { + if (!$menu_name) { return []; }