Skip to content

Commit

Permalink
add Twig Runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
garak committed Sep 5, 2024
1 parent 3481ca6 commit 1d4d896
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 84 deletions.
20 changes: 20 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,23 @@ parameters:
count: 1
path: src/Knp/Menu/Util/MenuManipulator.php

-
message: "#^Method Knp\\\\Menu\\\\Twig\\\\MenuExtension\\:\\:getBreadcrumbsArray\\(\\) has parameter \\$subItem with no value type specified in iterable type array\\.$#"
count: 1
path: src/Knp/Menu/Twig/MenuExtension.php

-
message: "#^PHPDoc tag @param for parameter \\$subItem with type array\\<int\\|string, array\\<string, Knp\\\\Menu\\\\ItemInterface\\|string\\|null\\>\\|float\\|int\\|Knp\\\\Menu\\\\ItemInterface\\|string\\|null\\>\\|string\\|Traversable\\<mixed, array\\<string, Knp\\\\Menu\\\\ItemInterface\\|string\\|null\\>\\|float\\|int\\|Knp\\\\Menu\\\\ItemInterface\\|string\\|null\\> is not subtype of native type array\\|string\\|null\\.$#"
count: 1
path: src/Knp/Menu/Twig/MenuExtension.php

-
message: "#^Method Knp\\\\Menu\\\\Twig\\\\MenuRuntimeExtension\\:\\:getBreadcrumbsArray\\(\\) has parameter \\$subItem with no value type specified in iterable type array\\.$#"
count: 1
path: src/Knp/Menu/Twig/MenuRuntimeExtension.php

-
message: "#^PHPDoc tag @param for parameter \\$subItem with type array\\<int\\|string, array\\<string, Knp\\\\Menu\\\\ItemInterface\\|string\\|null\\>\\|float\\|int\\|Knp\\\\Menu\\\\ItemInterface\\|string\\|null\\>\\|string\\|Traversable\\<mixed, array\\<string, Knp\\\\Menu\\\\ItemInterface\\|string\\|null\\>\\|float\\|int\\|Knp\\\\Menu\\\\ItemInterface\\|string\\|null\\> is not subtype of native type array\\|string\\|null\\.$#"
count: 1
path: src/Knp/Menu/Twig/MenuRuntimeExtension.php

119 changes: 47 additions & 72 deletions src/Knp/Menu/Twig/MenuExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,138 +12,113 @@

class MenuExtension extends AbstractExtension
{
public function __construct(private Helper $helper, private ?MatcherInterface $matcher = null, private ?MenuManipulator $menuManipulator = null)
{
private ?MenuRuntimeExtension $runtimeExtension = null;

public function __construct(
?Helper $helper = null,
?MatcherInterface $matcher = null,
?MenuManipulator $menuManipulator = null,
) {
if (null !== $helper) {
@trigger_error('Injecting dependencies is deprecated since v3.6 and will be removed in v4.', E_USER_DEPRECATED);
$this->runtimeExtension = new MenuRuntimeExtension($helper, $matcher, $menuManipulator);
}
}

/**
* @return array<int, TwigFunction>
*/
public function getFunctions(): array
{
$legacy = null !== $this->runtimeExtension;

return [
new TwigFunction('knp_menu_get', [$this, 'get']),
new TwigFunction('knp_menu_render', [$this, 'render'], ['is_safe' => ['html']]),
new TwigFunction('knp_menu_get_breadcrumbs_array', [$this, 'getBreadcrumbsArray']),
new TwigFunction('knp_menu_get_current_item', [$this, 'getCurrentItem']),
new TwigFunction('knp_menu_get', $legacy ? [$this, 'get'] : [MenuRuntimeExtension::class, 'get']),
new TwigFunction('knp_menu_render', $legacy ? [$this, 'render'] : [MenuRuntimeExtension::class, 'render'], ['is_safe' => ['html']]),
new TwigFunction('knp_menu_get_breadcrumbs_array', $legacy ? [$this, 'getBreadcrumbsArray'] : [MenuRuntimeExtension::class, 'getBreadcrumbsArray']),
new TwigFunction('knp_menu_get_current_item', $legacy ? [$this, 'getCurrentItem'] : [MenuRuntimeExtension::class, 'getCurrentItem']),
];
}

/**
* @return array<int, TwigFilter>
*/
public function getFilters(): array
{
$legacy = null !== $this->runtimeExtension;

return [
new TwigFilter('knp_menu_as_string', [$this, 'pathAsString']),
new TwigFilter('knp_menu_as_string', $legacy ? [$this, 'pathAsString'] : [MenuRuntimeExtension::class, 'pathAsString']),
];
}

/**
* @return array<int, TwigTest>
*/
public function getTests(): array
{
$legacy = null !== $this->runtimeExtension;

return [
new TwigTest('knp_menu_current', [$this, 'isCurrent']),
new TwigTest('knp_menu_ancestor', [$this, 'isAncestor']),
new TwigTest('knp_menu_current', $legacy ? [$this, 'isCurrent'] : [MenuRuntimeExtension::class, 'isCurrent']),
new TwigTest('knp_menu_ancestor', $legacy ? [$this, 'isAncestor'] : [MenuRuntimeExtension::class, 'isAncestor']),
];
}

/**
* Retrieves an item following a path in the tree.
*
* @param ItemInterface|string $menu
* @param array<int, string> $path
* @param array<string, mixed> $options
*/
public function get($menu, array $path = [], array $options = []): ItemInterface
public function get(ItemInterface|string $menu, array $path = [], array $options = []): ItemInterface
{
return $this->helper->get($menu, $path, $options);
assert(null !== $this->runtimeExtension);

return $this->runtimeExtension->get($menu, $path, $options);
}

/**
* Renders a menu with the specified renderer.
*
* @param ItemInterface|string|array<ItemInterface|string> $menu
* @param string|ItemInterface|array<ItemInterface|string> $menu
* @param array<string, mixed> $options
*/
public function render($menu, array $options = [], ?string $renderer = null): string
public function render(array|ItemInterface|string $menu, array $options = [], ?string $renderer = null): string
{
return $this->helper->render($menu, $options, $renderer);
assert(null !== $this->runtimeExtension);

return $this->runtimeExtension->render($menu, $options, $renderer);
}

/**
* Returns an array ready to be used for breadcrumbs.
*
* @param ItemInterface|string|array<ItemInterface|string> $menu
* @param string|ItemInterface|array<ItemInterface|string> $menu
* @param string|array<string|null>|null $subItem
*
* @phpstan-param string|ItemInterface|array<int|string, string|int|float|null|array{label: string, url: string|null, item: ItemInterface|null}|ItemInterface>|\Traversable<string|int|float|null|array{label: string, url: string|null, item: ItemInterface|null}|ItemInterface> $subItem
*
* @return array<int, array<string, mixed>>
* @phpstan-return list<array{label: string, uri: string|null, item: ItemInterface|null}>
*/
public function getBreadcrumbsArray($menu, $subItem = null): array
public function getBreadcrumbsArray(array|ItemInterface|string $menu, array|string|null $subItem = null): array
{
return $this->helper->getBreadcrumbsArray($menu, $subItem);
assert(null !== $this->runtimeExtension);

return $this->runtimeExtension->getBreadcrumbsArray($menu, $subItem);
}

/**
* Returns the current item of a menu.
*
* @param ItemInterface|string $menu
*/
public function getCurrentItem($menu): ItemInterface
public function getCurrentItem(ItemInterface|string $menu): ItemInterface
{
$rootItem = $this->get($menu);
assert(null !== $this->runtimeExtension);

$currentItem = $this->helper->getCurrentItem($rootItem);

if (null === $currentItem) {
$currentItem = $rootItem;
}

return $currentItem;
return $this->runtimeExtension->getCurrentItem($menu);
}

/**
* A string representation of this menu item
*
* e.g. Top Level > Second Level > This menu
*/
public function pathAsString(ItemInterface $menu, string $separator = ' > '): string
{
if (null === $this->menuManipulator) {
throw new \BadMethodCallException('The menu manipulator must be set to get the breadcrumbs array');
}
assert(null !== $this->runtimeExtension);

return $this->menuManipulator->getPathAsString($menu, $separator);
return $this->runtimeExtension->pathAsString($menu, $separator);
}

/**
* Checks whether an item is current.
*/
public function isCurrent(ItemInterface $item): bool
{
if (null === $this->matcher) {
throw new \BadMethodCallException('The matcher must be set to get the breadcrumbs array');
}
assert(null !== $this->runtimeExtension);

return $this->matcher->isCurrent($item);
return $this->runtimeExtension->isCurrent($item);
}

/**
* Checks whether an item is the ancestor of a current item.
*
* @param int|null $depth The max depth to look for the item
*/
public function isAncestor(ItemInterface $item, ?int $depth = null): bool
{
if (null === $this->matcher) {
throw new \BadMethodCallException('The matcher must be set to get the breadcrumbs array');
}
assert(null !== $this->runtimeExtension);

return $this->matcher->isAncestor($item, $depth);
return $this->runtimeExtension->isAncestor($item, $depth);
}
}
112 changes: 112 additions & 0 deletions src/Knp/Menu/Twig/MenuRuntimeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace Knp\Menu\Twig;

use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\MatcherInterface;
use Knp\Menu\Util\MenuManipulator;
use Twig\Extension\RuntimeExtensionInterface;

class MenuRuntimeExtension implements RuntimeExtensionInterface
{
public function __construct(
private readonly Helper $helper,
private readonly ?MatcherInterface $matcher = null,
private readonly ?MenuManipulator $menuManipulator = null,
) {
}

/**
* Retrieves an item following a path in the tree.
*
* @param array<int, string> $path
* @param array<string, mixed> $options
*/
public function get(ItemInterface|string $menu, array $path = [], array $options = []): ItemInterface
{
return $this->helper->get($menu, $path, $options);
}

/**
* Renders a menu with the specified renderer.
*
* @param string|ItemInterface|array<ItemInterface|string> $menu
* @param array<string, mixed> $options
*/
public function render(array|ItemInterface|string $menu, array $options = [], ?string $renderer = null): string
{
return $this->helper->render($menu, $options, $renderer);
}

/**
* Returns an array ready to be used for breadcrumbs.
*
* @param string|ItemInterface|array<ItemInterface|string> $menu
* @param string|array<string|null>|null $subItem
*
* @phpstan-param string|ItemInterface|array<int|string, string|int|float|null|array{label: string, url: string|null, item: ItemInterface|null}|ItemInterface>|\Traversable<string|int|float|null|array{label: string, url: string|null, item: ItemInterface|null}|ItemInterface> $subItem
*
* @return array<int, array<string, mixed>>
* @phpstan-return list<array{label: string, uri: string|null, item: ItemInterface|null}>
*/
public function getBreadcrumbsArray(array|ItemInterface|string $menu, array|string|null $subItem = null): array
{
return $this->helper->getBreadcrumbsArray($menu, $subItem);
}

/**
* Returns the current item of a menu.
*/
public function getCurrentItem(ItemInterface|string $menu): ItemInterface
{
$rootItem = $this->get($menu);

$currentItem = $this->helper->getCurrentItem($rootItem);

if (null === $currentItem) {
$currentItem = $rootItem;
}

return $currentItem;
}

/**
* A string representation of this menu item
*
* e.g. Top Level > Second Level > This menu
*/
public function pathAsString(ItemInterface $menu, string $separator = ' > '): string
{
if (null === $this->menuManipulator) {
throw new \BadMethodCallException('The menu manipulator must be set to get the breadcrumbs array');
}

return $this->menuManipulator->getPathAsString($menu, $separator);
}

/**
* Checks whether an item is current.
*/
public function isCurrent(ItemInterface $item): bool
{
if (null === $this->matcher) {
throw new \BadMethodCallException('The matcher must be set to get the breadcrumbs array');
}

return $this->matcher->isCurrent($item);
}

/**
* Checks whether an item is the ancestor of a current item.
*
* @param int|null $depth The max depth to look for the item
*/
public function isAncestor(ItemInterface $item, ?int $depth = null): bool
{
if (null === $this->matcher) {
throw new \BadMethodCallException('The matcher must be set to get the breadcrumbs array');
}

return $this->matcher->isAncestor($item, $depth);
}
}
27 changes: 15 additions & 12 deletions tests/Knp/Menu/Tests/Twig/MenuExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
use Knp\Menu\Matcher\MatcherInterface;
use Knp\Menu\Twig\Helper;
use Knp\Menu\Twig\MenuExtension;
use Knp\Menu\Twig\MenuRuntimeExtension;
use Knp\Menu\Util\MenuManipulator;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\RuntimeLoader\FactoryRuntimeLoader;
use Twig\TemplateWrapper;

final class MenuExtensionTest extends TestCase
Expand Down Expand Up @@ -162,10 +165,8 @@ public function testGetCurrentItem(): void

/**
* @param array<string> $methods
*
* @return Helper|\PHPUnit\Framework\MockObject\MockObject
*/
private function getHelperMock(array $methods)
private function getHelperMock(array $methods): MockObject|Helper
{
return $this->getMockBuilder(Helper::class)
->disableOriginalConstructor()
Expand All @@ -176,10 +177,8 @@ private function getHelperMock(array $methods)

/**
* @param array<string> $methods
*
* @return MenuManipulator|\PHPUnit\Framework\MockObject\MockObject
*/
private function getManipulatorMock(array $methods)
private function getManipulatorMock(array $methods): MenuManipulator|MockObject
{
return $this->getMockBuilder(MenuManipulator::class)
->disableOriginalConstructor()
Expand All @@ -188,10 +187,7 @@ private function getManipulatorMock(array $methods)
;
}

/**
* @return MatcherInterface|\PHPUnit\Framework\MockObject\MockObject
*/
private function getMatcherMock()
private function getMatcherMock(): MockObject|MatcherInterface
{
return $this->getMockBuilder(MatcherInterface::class)->getMock();
}
Expand All @@ -200,11 +196,18 @@ private function getTemplate(
string $template,
Helper $helper,
?MatcherInterface $matcher = null,
?MenuManipulator $menuManipulator = null
?MenuManipulator $menuManipulator = null,
): TemplateWrapper {
$loader = new ArrayLoader(['index' => $template]);
$twig = new Environment($loader, ['debug' => true, 'cache' => false]);
$twig->addExtension(new MenuExtension($helper, $matcher, $menuManipulator));
$twig->addExtension(new MenuExtension());
$twig->addRuntimeLoader(new FactoryRuntimeLoader([
MenuRuntimeExtension::class => fn () => new MenuRuntimeExtension(
$helper,
$matcher,
$menuManipulator,
),
]));

return $twig->load('index');
}
Expand Down

0 comments on commit 1d4d896

Please sign in to comment.