Actualización

This commit is contained in:
Xes
2025-04-10 12:24:57 +02:00
parent 8969cc929d
commit 45420b6f0d
39760 changed files with 4303286 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
<?php
namespace Knp\Menu\Factory;
use Knp\Menu\ItemInterface;
/**
* core factory extension with the main logic
*/
class CoreExtension implements ExtensionInterface
{
/**
* Builds the full option array used to configure the item.
*
* @param array $options
*
* @return array
*/
public function buildOptions(array $options)
{
return \array_merge(
[
'uri' => null,
'label' => null,
'attributes' => [],
'linkAttributes' => [],
'childrenAttributes' => [],
'labelAttributes' => [],
'extras' => [],
'current' => null,
'display' => true,
'displayChildren' => true,
],
$options
);
}
/**
* Configures the newly created item with the passed options
*
* @param ItemInterface $item
* @param array $options
*/
public function buildItem(ItemInterface $item, array $options)
{
$item
->setUri($options['uri'])
->setLabel($options['label'])
->setAttributes($options['attributes'])
->setLinkAttributes($options['linkAttributes'])
->setChildrenAttributes($options['childrenAttributes'])
->setLabelAttributes($options['labelAttributes'])
->setCurrent($options['current'])
->setDisplay($options['display'])
->setDisplayChildren($options['displayChildren'])
;
$this->buildExtras($item, $options);
}
/**
* Configures the newly created item's extras
* Extras are processed one by one in order not to reset values set by other extensions
*
* @param ItemInterface $item
* @param array $options
*/
private function buildExtras(ItemInterface $item, array $options)
{
if (!empty($options['extras'])) {
foreach ($options['extras'] as $key => $value) {
$item->setExtra($key, $value);
}
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Knp\Menu\Factory;
use Knp\Menu\ItemInterface;
interface ExtensionInterface
{
/**
* Builds the full option array used to configure the item.
*
* @param array $options The options processed by the previous extensions
*
* @return array
*/
public function buildOptions(array $options);
/**
* Configures the item with the passed options
*
* @param ItemInterface $item
* @param array $options
*/
public function buildItem(ItemInterface $item, array $options);
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Knp\Menu;
/**
* Interface implemented by the factory to create items
*/
interface FactoryInterface
{
/**
* Creates a menu item
*
* @param string $name
* @param array $options
*
* @return ItemInterface
*/
public function createItem($name, array $options = []);
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Knp\Menu\Integration\Silex;
@trigger_error('The '.__NAMESPACE__.'\KnpMenuServiceProvider class is deprecated since version 2.3 and will be removed in 3.0. Use the provider available in the "knplabs/knp-menu-silex" package instead.', E_USER_DEPRECATED);
use Knp\Menu\Integration\Symfony\RoutingExtension;
use Silex\Application;
use Silex\ServiceProviderInterface;
use Knp\Menu\Matcher\Matcher;
use Knp\Menu\MenuFactory;
use Knp\Menu\Renderer\ListRenderer;
use Knp\Menu\Renderer\TwigRenderer;
use Knp\Menu\Provider\ArrayAccessProvider as PimpleMenuProvider;
use Knp\Menu\Renderer\ArrayAccessProvider as PimpleRendererProvider;
use Knp\Menu\Twig\Helper;
use Knp\Menu\Twig\MenuExtension;
use Knp\Menu\Util\MenuManipulator;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
class KnpMenuServiceProvider implements ServiceProviderInterface
{
public function register(Application $app)
{
$app['knp_menu.factory'] = $app->share(function () use ($app) {
$factory = new MenuFactory();
if (isset($app['url_generator'])) {
$factory->addExtension(new RoutingExtension($app['url_generator']));
}
return $factory;
});
$app['knp_menu.matcher'] = $app->share(function () use ($app) {
$matcher = new Matcher();
if (isset($app['knp_menu.matcher.configure'])) {
$app['knp_menu.matcher.configure']($matcher);
}
return $matcher;
});
$app['knp_menu.renderer.list'] = $app->share(function () use ($app) {
return new ListRenderer($app['knp_menu.matcher'], [], $app['charset']);
});
$app['knp_menu.menu_provider'] = $app->share(function () use ($app) {
return new PimpleMenuProvider($app, $app['knp_menu.menus']);
});
if (!isset($app['knp_menu.menus'])) {
$app['knp_menu.menus'] = [];
}
$app['knp_menu.renderer_provider'] = $app->share(function () use ($app) {
$app['knp_menu.renderers'] = array_merge(
['list' => 'knp_menu.renderer.list'],
isset($app['knp_menu.renderer.twig']) ? ['twig' => 'knp_menu.renderer.twig'] : [],
isset($app['knp_menu.renderers']) ? $app['knp_menu.renderers'] : []
);
return new PimpleRendererProvider($app, $app['knp_menu.default_renderer'], $app['knp_menu.renderers']);
});
$app['knp_menu.menu_manipulator'] = $app->share(function () use ($app) {
return new MenuManipulator();
});
if (!isset($app['knp_menu.default_renderer'])) {
$app['knp_menu.default_renderer'] = 'list';
}
$app['knp_menu.helper'] = $app->share(function () use ($app) {
return new Helper($app['knp_menu.renderer_provider'], $app['knp_menu.menu_provider'], $app['knp_menu.menu_manipulator'], $app['knp_menu.matcher']);
});
if (isset($app['twig'])) {
$app['knp_menu.twig_extension'] = $app->share(function () use ($app) {
return new MenuExtension($app['knp_menu.helper'], $app['knp_menu.matcher'], $app['knp_menu.menu_manipulator']);
});
$app['knp_menu.renderer.twig'] = $app->share(function () use ($app) {
return new TwigRenderer($app['twig'], $app['knp_menu.template'], $app['knp_menu.matcher']);
});
if (!isset($app['knp_menu.template'])) {
$app['knp_menu.template'] = 'knp_menu.html.twig';
}
$app['twig'] = $app->share($app->extend('twig', function (Environment $twig) use ($app) {
$twig->addExtension($app['knp_menu.twig_extension']);
return $twig;
}));
$app['twig.loader.filesystem'] = $app->share($app->extend('twig.loader.filesystem', function (FilesystemLoader $loader) use ($app) {
$loader->addPath(__DIR__.'/../../Resources/views');
return $loader;
}));
}
}
public function boot(Application $app) {}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Knp\Menu\Integration\Symfony;
use Knp\Menu\Factory\ExtensionInterface;
use Knp\Menu\ItemInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Factory able to use the Symfony Routing component to build the url
*/
class RoutingExtension implements ExtensionInterface
{
private $generator;
public function __construct(UrlGeneratorInterface $generator)
{
$this->generator = $generator;
}
public function buildOptions(array $options = [])
{
if (!empty($options['route'])) {
$params = isset($options['routeParameters']) ? $options['routeParameters'] : [];
$absolute = (isset($options['routeAbsolute']) && $options['routeAbsolute']) ? UrlGeneratorInterface::ABSOLUTE_URL : UrlGeneratorInterface::ABSOLUTE_PATH;
$options['uri'] = $this->generator->generate($options['route'], $params, $absolute);
// adding the item route to the extras under the 'routes' key (for the Silex RouteVoter)
$options['extras']['routes'][] = [
'route' => $options['route'],
'parameters' => $params,
];
}
return $options;
}
public function buildItem(ItemInterface $item, array $options)
{
}
}

View File

@@ -0,0 +1,457 @@
<?php
namespace Knp\Menu;
/**
* Interface implemented by a menu item.
*
* It roughly represents a single <li> tag and is what you should interact with
* most of the time by default.
* Originally taken from ioMenuPlugin (http://github.com/weaverryan/ioMenuPlugin)
*
* @extends \IteratorAggregate<string, self>
*/
interface ItemInterface extends \ArrayAccess, \Countable, \IteratorAggregate
{
/**
* @param FactoryInterface $factory
*
* @return ItemInterface
*/
public function setFactory(FactoryInterface $factory);
/**
* @return string
*/
public function getName();
/**
* Renames the item.
*
* This method must also update the key in the parent.
*
* Provides a fluent interface
*
* @param string $name
*
* @return ItemInterface
*
* @throws \InvalidArgumentException if the name is already used by a sibling
*/
public function setName($name);
/**
* Get the uri for a menu item
*
* @return string
*/
public function getUri();
/**
* Set the uri for a menu item
*
* Provides a fluent interface
*
* @param string $uri The uri to set on this menu item
*
* @return ItemInterface
*/
public function setUri($uri);
/**
* Returns the label that will be used to render this menu item
*
* Defaults to the name of no label was specified
*
* @return string|null
*/
public function getLabel();
/**
* Provides a fluent interface
*
* @param string|null $label The text to use when rendering this menu item
*
* @return ItemInterface
*/
public function setLabel($label);
/**
* @return array
*/
public function getAttributes();
/**
* Provides a fluent interface
*
* @param array $attributes
*
* @return ItemInterface
*/
public function setAttributes(array $attributes);
/**
* @param string $name The name of the attribute to return
* @param mixed $default The value to return if the attribute doesn't exist
*
* @return mixed
*/
public function getAttribute($name, $default = null);
/**
* Provides a fluent interface
*
* @param string $name
* @param mixed $value
*
* @return ItemInterface
*/
public function setAttribute($name, $value);
/**
* @return array
*/
public function getLinkAttributes();
/**
* Provides a fluent interface
*
* @param array $linkAttributes
*
* @return ItemInterface
*/
public function setLinkAttributes(array $linkAttributes);
/**
* @param string $name The name of the attribute to return
* @param mixed $default The value to return if the attribute doesn't exist
*
* @return mixed
*/
public function getLinkAttribute($name, $default = null);
/**
* Provides a fluent interface
*
* @param string $name
* @param string $value
*
* @return ItemInterface
*/
public function setLinkAttribute($name, $value);
/**
* @return array
*/
public function getChildrenAttributes();
/**
* Provides a fluent interface
*
* @param array $childrenAttributes
*
* @return ItemInterface
*/
public function setChildrenAttributes(array $childrenAttributes);
/**
* @param string $name The name of the attribute to return
* @param mixed $default The value to return if the attribute doesn't exist
*
* @return mixed
*/
public function getChildrenAttribute($name, $default = null);
/**
* Provides a fluent interface
*
* @param string $name
* @param string $value
*
* @return ItemInterface
*/
public function setChildrenAttribute($name, $value);
/**
* @return array
*/
public function getLabelAttributes();
/**
* Provides a fluent interface
*
* @param array $labelAttributes
*
* @return ItemInterface
*/
public function setLabelAttributes(array $labelAttributes);
/**
* @param string $name The name of the attribute to return
* @param mixed $default The value to return if the attribute doesn't exist
*
* @return mixed
*/
public function getLabelAttribute($name, $default = null);
/**
* Provides a fluent interface
*
* @param string $name
* @param mixed $value
*
* @return ItemInterface
*/
public function setLabelAttribute($name, $value);
/**
* @return array
*/
public function getExtras();
/**
* Provides a fluent interface
*
* @param array $extras
*
* @return ItemInterface
*/
public function setExtras(array $extras);
/**
* @param string $name The name of the extra to return
* @param mixed $default The value to return if the extra doesn't exist
*
* @return mixed
*/
public function getExtra($name, $default = null);
/**
* Provides a fluent interface
*
* @param string $name
* @param mixed $value
*
* @return ItemInterface
*/
public function setExtra($name, $value);
/**
* Whether or not this menu item should show its children.
*
* @return boolean
*/
public function getDisplayChildren();
/**
* Set whether or not this menu item should show its children
*
* Provides a fluent interface
*
* @param bool $bool
*
* @return ItemInterface
*/
public function setDisplayChildren($bool);
/**
* Whether or not to display this menu item
*
* @return bool
*/
public function isDisplayed();
/**
* Set whether or not this menu should be displayed
*
* Provides a fluent interface
*
* @param bool $bool
*
* @return ItemInterface
*/
public function setDisplay($bool);
/**
* Add a child menu item to this menu
*
* Returns the child item
*
* @param ItemInterface|string $child An ItemInterface instance or the name of a new item to create
* @param array $options If creating a new item, the options passed to the factory for the item
*
* @return ItemInterface
*
* @throws \InvalidArgumentException if the item is already in a tree
*/
public function addChild($child, array $options = []);
/**
* Returns the child menu identified by the given name
*
* @param string $name Then name of the child menu to return
*
* @return ItemInterface|null
*/
public function getChild($name);
/**
* Reorder children.
*
* Provides a fluent interface
*
* @param array $order New order of children.
*
* @return ItemInterface
*/
public function reorderChildren($order);
/**
* Makes a deep copy of menu tree. Every item is copied as another object.
*
* @return ItemInterface
*/
public function copy();
/**
* Returns the level of this menu item
*
* The root menu item is 0, followed by 1, 2, etc
*
* @return int
*/
public function getLevel();
/**
* Returns the root ItemInterface of this menu tree
*
* @return ItemInterface
*/
public function getRoot();
/**
* Returns whether or not this menu item is the root menu item
*
* @return bool
*/
public function isRoot();
/**
* @return ItemInterface|null
*/
public function getParent();
/**
* Used internally when adding and removing children
*
* Provides a fluent interface
*
* @param ItemInterface|null $parent
*
* @return ItemInterface
*/
public function setParent(ItemInterface $parent = null);
/**
* Return the children as an array of ItemInterface objects
*
* @return ItemInterface[]
*/
public function getChildren();
/**
* Provides a fluent interface
*
* @param array $children An array of ItemInterface objects
*
* @return ItemInterface
*/
public function setChildren(array $children);
/**
* Removes a child from this menu item
*
* Provides a fluent interface
*
* @param ItemInterface|string $name The name of ItemInterface instance or the ItemInterface to remove
*
* @return ItemInterface
*/
public function removeChild($name);
/**
* @return ItemInterface
*/
public function getFirstChild();
/**
* @return ItemInterface
*/
public function getLastChild();
/**
* Returns whether or not this menu items has viewable children
*
* This menu MAY have children, but this will return false if the current
* user does not have access to view any of those items
*
* @return bool
*/
public function hasChildren();
/**
* Sets whether or not this menu item is "current".
*
* If the state is unknown, use null.
*
* Provides a fluent interface
*
* @param bool|null $bool Specify that this menu item is current
*
* @return ItemInterface
*/
public function setCurrent($bool);
/**
* Gets whether or not this menu item is "current".
*
* @return bool|null
*/
public function isCurrent();
/**
* Whether this menu item is last in its parent
*
* @return bool
*/
public function isLast();
/**
* Whether this menu item is first in its parent
*
* @return bool
*/
public function isFirst();
/**
* Whereas isFirst() returns if this is the first child of the parent
* menu item, this function takes into consideration whether children are rendered or not.
*
* This returns true if this is the first child that would be rendered
* for the current user
*
* @return bool
*/
public function actsLikeFirst();
/**
* Whereas isLast() returns if this is the last child of the parent
* menu item, this function takes into consideration whether children are rendered or not.
*
* This returns true if this is the last child that would be rendered
* for the current user
*
* @return bool
*/
public function actsLikeLast();
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Knp\Menu\Iterator;
use Knp\Menu\Matcher\MatcherInterface;
/**
* Filter iterator keeping only current items
*/
class CurrentItemFilterIterator extends \FilterIterator
{
private $matcher;
public function __construct(\Iterator $iterator, MatcherInterface $matcher)
{
$this->matcher = $matcher;
parent::__construct($iterator);
}
public function accept()
{
return $this->matcher->isCurrent($this->current());
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Knp\Menu\Iterator;
/**
* Filter iterator keeping only current items
*/
class DisplayedItemFilterIterator extends \RecursiveFilterIterator
{
public function accept()
{
return $this->current()->isDisplayed();
}
public function hasChildren()
{
return $this->current()->getDisplayChildren() && parent::hasChildren();
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Knp\Menu\Iterator;
/**
* Recursive iterator iterating on an item
*/
class RecursiveItemIterator extends \IteratorIterator implements \RecursiveIterator
{
public function hasChildren()
{
return 0 < \count($this->current());
}
public function getChildren()
{
return new static($this->current());
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Knp\Menu\Loader;
use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
/**
* Loader importing a menu tree from an array.
*
* The array should match the output of MenuManipulator::toArray
*/
class ArrayLoader implements LoaderInterface
{
private $factory;
public function __construct(FactoryInterface $factory)
{
$this->factory = $factory;
}
public function load($data)
{
if (!$this->supports($data)) {
throw new \InvalidArgumentException(\sprintf('Unsupported data. Expected an array but got %s', \is_object($data) ? \get_class($data) : \gettype($data)));
}
return $this->fromArray($data);
}
public function supports($data)
{
return \is_array($data);
}
/**
* @param array $data
* @param string|null $name (the name of the item, used only if there is no name in the data themselves)
*
* @return ItemInterface
*/
private function fromArray(array $data, $name = null)
{
$name = isset($data['name']) ? $data['name'] : $name;
if (isset($data['children'])) {
$children = $data['children'];
unset($data['children']);
} else {
$children = [];
}
$item = $this->factory->createItem($name, $data);
foreach ($children as $name => $child) {
$item->addChild($this->fromArray($child, $name));
}
return $item;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Knp\Menu\Loader;
use Knp\Menu\ItemInterface;
interface LoaderInterface
{
/**
* Loads the data into a menu item
*
* @param mixed $data
*
* @return ItemInterface
*/
public function load($data);
/**
* Checks whether the loader can load these data
*
* @param mixed $data
*
* @return bool
*/
public function supports($data);
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Knp\Menu\Loader;
use Knp\Menu\FactoryInterface;
use Knp\Menu\NodeInterface;
class NodeLoader implements LoaderInterface
{
private $factory;
public function __construct(FactoryInterface $factory)
{
$this->factory = $factory;
}
public function load($data)
{
if (!$data instanceof NodeInterface) {
throw new \InvalidArgumentException(\sprintf('Unsupported data. Expected Knp\Menu\NodeInterface but got %s', \is_object($data) ? \get_class($data) : \gettype($data)));
}
$item = $this->factory->createItem($data->getName(), $data->getOptions());
foreach ($data->getChildren() as $childNode) {
$item->addChild($this->load($childNode));
}
return $item;
}
public function supports($data)
{
return $data instanceof NodeInterface;
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Knp\Menu\Matcher;
use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Voter\VoterInterface;
/**
* A MatcherInterface implementation using a voter system
*/
class Matcher implements MatcherInterface
{
private $cache;
private $voters;
/**
* @param VoterInterface[]|iterable $voters
*/
public function __construct($voters = [])
{
$this->voters = $voters;
$this->cache = new \SplObjectStorage();
}
/**
* Adds a voter in the matcher.
*
* If an iterator was used to provide voters in the constructor, it will be
* converted to array when using this method, breaking any potential lazy-loading.
*
* @deprecated since 2.3. Pass voters in the constructor instead.
*
* @param VoterInterface $voter
*/
public function addVoter(VoterInterface $voter)
{
@trigger_error(\sprintf('The %s() method is deprecated since version 2.3 and will be removed in 3.0. Pass voters in the constructor instead.', __METHOD__), E_USER_DEPRECATED);
if ($this->voters instanceof \Traversable) {
$this->voters = \iterator_to_array($this->voters);
}
$this->voters[] = $voter;
}
public function isCurrent(ItemInterface $item)
{
$current = $item->isCurrent();
if (null !== $current) {
return $current;
}
if ($this->cache->contains($item)) {
return $this->cache[$item];
}
foreach ($this->voters as $voter) {
$current = $voter->matchItem($item);
if (null !== $current) {
break;
}
}
$current = (bool) $current;
$this->cache[$item] = $current;
return $current;
}
public function isAncestor(ItemInterface $item, $depth = null)
{
if (0 === $depth) {
return false;
}
$childDepth = null === $depth ? null : $depth - 1;
foreach ($item->getChildren() as $child) {
if ($this->isCurrent($child) || $this->isAncestor($child, $childDepth)) {
return true;
}
}
return false;
}
public function clear()
{
$this->cache = new \SplObjectStorage();
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Knp\Menu\Matcher;
use Knp\Menu\ItemInterface;
/**
* Interface implemented by the item matcher
*/
interface MatcherInterface
{
/**
* Checks whether an item is current.
*
* @param ItemInterface $item
*
* @return bool
*/
public function isCurrent(ItemInterface $item);
/**
* Checks whether an item is the ancestor of a current item.
*
* @param ItemInterface $item
* @param int|null $depth The max depth to look for the item
*
* @return bool
*/
public function isAncestor(ItemInterface $item, $depth = null);
/**
* Clears the state of the matcher.
*/
public function clear();
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Knp\Menu\Matcher\Voter;
use Knp\Menu\ItemInterface;
/**
* Implements the VoterInterface which can be used as voter for "current" class
* `matchItem` will return true if the pattern you're searching for is found in the URI of the item
*/
class RegexVoter implements VoterInterface
{
/**
* @var string|null
*/
private $regexp;
/**
* @param string|null $regexp
*/
public function __construct($regexp)
{
$this->regexp = $regexp;
}
public function matchItem(ItemInterface $item)
{
if (null === $this->regexp || null === $item->getUri()) {
return null;
}
if (\preg_match($this->regexp, $item->getUri())) {
return true;
}
return null;
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace Knp\Menu\Matcher\Voter;
use Knp\Menu\ItemInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Voter based on the route
*/
class RouteVoter implements VoterInterface
{
/**
* @var RequestStack|null
*/
private $requestStack;
/**
* @var Request|null
*/
private $request;
public function __construct($requestStack = null)
{
if ($requestStack instanceof RequestStack) {
$this->requestStack = $requestStack;
} elseif ($requestStack instanceof Request) {
@trigger_error(sprintf('Passing a Request as the first argument for "%s" constructor is deprecated since version 2.3 and won\'t be possible in 3.0. Pass a RequestStack instead.', __CLASS__), E_USER_DEPRECATED);
// BC layer for the old API of the class
$this->request = $requestStack;
} elseif (null !== $requestStack) {
throw new \InvalidArgumentException('The first argument of %s must be null, a RequestStack or a Request. %s given', __CLASS__, is_object($requestStack) ? get_class($requestStack) : gettype($requestStack));
} else {
@trigger_error(sprintf('Not passing a RequestStack as the first argument for "%s" constructor is deprecated since version 2.3 and won\'t be possible in 3.0.', __CLASS__), E_USER_DEPRECATED);
}
}
/**
* Sets the request against which the menu should be matched.
*
* This Request is ignored in case a RequestStack is passed in the constructor.
*
* @deprecated since version 2.3. Pass a RequestStack to the constructor instead.
*
* @param Request $request
*/
public function setRequest(Request $request)
{
@trigger_error(\sprintf('The %s() method is deprecated since version 2.3 and will be removed in 3.0. Pass a RequestStack in the constructor instead.', __METHOD__), E_USER_DEPRECATED);
$this->request = $request;
}
public function matchItem(ItemInterface $item)
{
if (null !== $this->requestStack) {
$request = $this->requestStack->getMasterRequest();
} else {
$request = $this->request;
}
if (null === $request) {
return null;
}
$route = $request->attributes->get('_route');
if (null === $route) {
return null;
}
$routes = (array) $item->getExtra('routes', []);
foreach ($routes as $testedRoute) {
if (\is_string($testedRoute)) {
$testedRoute = ['route' => $testedRoute];
}
if (!\is_array($testedRoute)) {
throw new \InvalidArgumentException('Routes extra items must be strings or arrays.');
}
if ($this->isMatchingRoute($request, $testedRoute)) {
return true;
}
}
return null;
}
private function isMatchingRoute(Request $request, array $testedRoute)
{
$route = $request->attributes->get('_route');
if (isset($testedRoute['route'])) {
if ($route !== $testedRoute['route']) {
return false;
}
} elseif (!empty($testedRoute['pattern'])) {
if (!\preg_match($testedRoute['pattern'], $route)) {
return false;
}
} else {
throw new \InvalidArgumentException('Routes extra items must have a "route" or "pattern" key.');
}
if (!isset($testedRoute['parameters'])) {
return true;
}
$routeParameters = $request->attributes->get('_route_params', []);
foreach ($testedRoute['parameters'] as $name => $value) {
// cast both to string so that we handle integer and other non-string parameters, but don't stumble on 0 == 'abc'.
if (!isset($routeParameters[$name]) || (string) $routeParameters[$name] !== (string) $value) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Knp\Menu\Matcher\Voter;
use Knp\Menu\ItemInterface;
/**
* Voter based on the uri
*/
class UriVoter implements VoterInterface
{
private $uri;
public function __construct($uri = null)
{
$this->uri = $uri;
}
public function matchItem(ItemInterface $item)
{
if (null === $this->uri || null === $item->getUri()) {
return null;
}
if ($item->getUri() === $this->uri) {
return true;
}
return null;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Knp\Menu\Matcher\Voter;
use Knp\Menu\ItemInterface;
/**
* Interface implemented by the matching voters
*/
interface VoterInterface
{
/**
* Checks whether an item is current.
*
* If the voter is not able to determine a result,
* it should return null to let other voters do the job.
*
* @param ItemInterface $item
*
* @return bool|null
*/
public function matchItem(ItemInterface $item);
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Knp\Menu;
use Knp\Menu\Factory\CoreExtension;
use Knp\Menu\Factory\ExtensionInterface;
/**
* Factory to create a menu from a tree
*/
class MenuFactory implements FactoryInterface
{
/**
* @var array[]
*/
private $extensions = [];
/**
* @var ExtensionInterface[]|null
*/
private $sorted;
public function __construct()
{
$this->addExtension(new CoreExtension(), -10);
}
public function createItem($name, array $options = [])
{
foreach ($this->getExtensions() as $extension) {
$options = $extension->buildOptions($options);
}
$item = new MenuItem($name, $this);
foreach ($this->getExtensions() as $extension) {
$extension->buildItem($item, $options);
}
return $item;
}
/**
* Adds a factory extension
*
* @param ExtensionInterface $extension
* @param int $priority
*/
public function addExtension(ExtensionInterface $extension, $priority = 0)
{
$this->extensions[$priority][] = $extension;
$this->sorted = null;
}
/**
* Sorts the internal list of extensions by priority.
*
* @return ExtensionInterface[]|null
*/
private function getExtensions()
{
if (null === $this->sorted) {
\krsort($this->extensions);
$this->sorted = !empty($this->extensions) ? \call_user_func_array('array_merge', $this->extensions) : [];
}
return $this->sorted;
}
}

View File

@@ -0,0 +1,627 @@
<?php
namespace Knp\Menu;
/**
* Default implementation of the ItemInterface
*/
class MenuItem implements ItemInterface
{
/**
* Name of this menu item (used for id by parent menu)
*
* @var string
*/
protected $name;
/**
* Label to output, name is used by default
*
* @var string|null
*/
protected $label;
/**
* Attributes for the item link
*
* @var array
*/
protected $linkAttributes = [];
/**
* Attributes for the children list
*
* @var array
*/
protected $childrenAttributes = [];
/**
* Attributes for the item text
*
* @var array
*/
protected $labelAttributes = [];
/**
* Uri to use in the anchor tag
*
* @var string|null
*/
protected $uri;
/**
* Attributes for the item
*
* @var array
*/
protected $attributes = [];
/**
* Extra stuff associated to the item
*
* @var array
*/
protected $extras = [];
/**
* Whether the item is displayed
*
* @var bool
*/
protected $display = true;
/**
* Whether the children of the item are displayed
*
* @var bool
*/
protected $displayChildren = true;
/**
* Child items
*
* @var ItemInterface[]
*/
protected $children = [];
/**
* Parent item
*
* @var ItemInterface|null
*/
protected $parent;
/**
* whether the item is current. null means unknown
*
* @var bool|null
*/
protected $isCurrent;
/**
* @var FactoryInterface
*/
protected $factory;
/**
* Class constructor
*
* @param string $name The name of this menu, which is how its parent will
* reference it. Also used as label if label not specified
* @param FactoryInterface $factory
*/
public function __construct($name, FactoryInterface $factory)
{
if (null === $name) {
@trigger_error('Passing a null name is deprecated since version 2.5 and will be removed in 3.0.', E_USER_DEPRECATED);
}
$this->name = (string) $name;
$this->factory = $factory;
}
/**
* setFactory
*
* @param FactoryInterface $factory
*
* @return ItemInterface
*/
public function setFactory(FactoryInterface $factory)
{
$this->factory = $factory;
return $this;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
if ($this->name === $name) {
return $this;
}
$parent = $this->getParent();
if (null !== $parent && isset($parent[$name])) {
throw new \InvalidArgumentException('Cannot rename item, name is already used by sibling.');
}
$oldName = $this->name;
$this->name = $name;
if (null !== $parent) {
$names = \array_keys($parent->getChildren());
$items = \array_values($parent->getChildren());
$offset = \array_search($oldName, $names);
$names[$offset] = $name;
$parent->setChildren(\array_combine($names, $items));
}
return $this;
}
public function getUri()
{
return $this->uri;
}
public function setUri($uri)
{
$this->uri = $uri;
return $this;
}
public function getLabel()
{
return (null !== $this->label) ? $this->label : $this->name;
}
public function setLabel($label)
{
$this->label = $label;
return $this;
}
public function getAttributes()
{
return $this->attributes;
}
public function setAttributes(array $attributes)
{
$this->attributes = $attributes;
return $this;
}
public function getAttribute($name, $default = null)
{
if (isset($this->attributes[$name])) {
return $this->attributes[$name];
}
return $default;
}
public function setAttribute($name, $value)
{
$this->attributes[$name] = $value;
return $this;
}
public function getLinkAttributes()
{
return $this->linkAttributes;
}
public function setLinkAttributes(array $linkAttributes)
{
$this->linkAttributes = $linkAttributes;
return $this;
}
public function getLinkAttribute($name, $default = null)
{
if (isset($this->linkAttributes[$name])) {
return $this->linkAttributes[$name];
}
return $default;
}
public function setLinkAttribute($name, $value)
{
$this->linkAttributes[$name] = $value;
return $this;
}
public function getChildrenAttributes()
{
return $this->childrenAttributes;
}
public function setChildrenAttributes(array $childrenAttributes)
{
$this->childrenAttributes = $childrenAttributes;
return $this;
}
public function getChildrenAttribute($name, $default = null)
{
if (isset($this->childrenAttributes[$name])) {
return $this->childrenAttributes[$name];
}
return $default;
}
public function setChildrenAttribute($name, $value)
{
$this->childrenAttributes[$name] = $value;
return $this;
}
public function getLabelAttributes()
{
return $this->labelAttributes;
}
public function setLabelAttributes(array $labelAttributes)
{
$this->labelAttributes = $labelAttributes;
return $this;
}
public function getLabelAttribute($name, $default = null)
{
if (isset($this->labelAttributes[$name])) {
return $this->labelAttributes[$name];
}
return $default;
}
public function setLabelAttribute($name, $value)
{
$this->labelAttributes[$name] = $value;
return $this;
}
public function getExtras()
{
return $this->extras;
}
public function setExtras(array $extras)
{
$this->extras = $extras;
return $this;
}
public function getExtra($name, $default = null)
{
if (isset($this->extras[$name])) {
return $this->extras[$name];
}
return $default;
}
public function setExtra($name, $value)
{
$this->extras[$name] = $value;
return $this;
}
public function getDisplayChildren()
{
return $this->displayChildren;
}
public function setDisplayChildren($bool)
{
$this->displayChildren = (bool) $bool;
return $this;
}
public function isDisplayed()
{
return $this->display;
}
public function setDisplay($bool)
{
$this->display = (bool) $bool;
return $this;
}
public function addChild($child, array $options = [])
{
if (!$child instanceof ItemInterface) {
$child = $this->factory->createItem($child, $options);
} elseif (null !== $child->getParent()) {
throw new \InvalidArgumentException('Cannot add menu item as child, it already belongs to another menu (e.g. has a parent).');
}
$child->setParent($this);
$this->children[$child->getName()] = $child;
return $child;
}
public function getChild($name)
{
return isset($this->children[$name]) ? $this->children[$name] : null;
}
public function reorderChildren($order)
{
if (count($order) != $this->count()) {
throw new \InvalidArgumentException('Cannot reorder children, order does not contain all children.');
}
$newChildren = [];
foreach ($order as $name) {
if (!isset($this->children[$name])) {
throw new \InvalidArgumentException('Cannot find children named ' . $name);
}
$child = $this->children[$name];
$newChildren[$name] = $child;
}
$this->setChildren($newChildren);
return $this;
}
public function copy()
{
$newMenu = clone $this;
$newMenu->setChildren([]);
$newMenu->setParent(null);
foreach ($this->getChildren() as $child) {
$newMenu->addChild($child->copy());
}
return $newMenu;
}
public function getLevel()
{
return $this->parent ? $this->parent->getLevel() + 1 : 0;
}
public function getRoot()
{
$obj = $this;
do {
$found = $obj;
} while ($obj = $obj->getParent());
return $found;
}
public function isRoot()
{
return null === $this->parent;
}
public function getParent()
{
return $this->parent;
}
public function setParent(ItemInterface $parent = null)
{
if ($parent === $this) {
throw new \InvalidArgumentException('Item cannot be a child of itself');
}
$this->parent = $parent;
return $this;
}
public function getChildren()
{
return $this->children;
}
public function setChildren(array $children)
{
$this->children = $children;
return $this;
}
public function removeChild($name)
{
$name = $name instanceof ItemInterface ? $name->getName() : $name;
if (isset($this->children[$name])) {
// unset the child and reset it so it looks independent
$this->children[$name]->setParent(null);
unset($this->children[$name]);
}
return $this;
}
public function getFirstChild()
{
return \reset($this->children);
}
public function getLastChild()
{
return \end($this->children);
}
public function hasChildren()
{
foreach ($this->children as $child) {
if ($child->isDisplayed()) {
return true;
}
}
return false;
}
public function setCurrent($bool)
{
$this->isCurrent = $bool;
return $this;
}
public function isCurrent()
{
return $this->isCurrent;
}
public function isLast()
{
// if this is root, then return false
if ($this->isRoot()) {
return false;
}
return $this->getParent()->getLastChild() === $this;
}
public function isFirst()
{
// if this is root, then return false
if ($this->isRoot()) {
return false;
}
return $this->getParent()->getFirstChild() === $this;
}
public function actsLikeFirst()
{
// root items are never "marked" as first
if ($this->isRoot()) {
return false;
}
// A menu acts like first only if it is displayed
if (!$this->isDisplayed()) {
return false;
}
// if we're first and visible, we're first, period.
if ($this->isFirst()) {
return true;
}
$children = $this->getParent()->getChildren();
foreach ($children as $child) {
// loop until we find a visible menu. If its this menu, we're first
if ($child->isDisplayed()) {
return $child->getName() === $this->getName();
}
}
return false;
}
public function actsLikeLast()
{
// root items are never "marked" as last
if ($this->isRoot()) {
return false;
}
// A menu acts like last only if it is displayed
if (!$this->isDisplayed()) {
return false;
}
// if we're last and visible, we're last, period.
if ($this->isLast()) {
return true;
}
$children = \array_reverse($this->getParent()->getChildren());
foreach ($children as $child) {
// loop until we find a visible menu. If its this menu, we're first
if ($child->isDisplayed()) {
return $child->getName() === $this->getName();
}
}
return false;
}
/**
* Implements Countable
*/
public function count()
{
return \count($this->children);
}
/**
* Implements IteratorAggregate
*/
public function getIterator()
{
return new \ArrayIterator($this->children);
}
/**
* Implements ArrayAccess
*/
public function offsetExists($name)
{
return isset($this->children[$name]);
}
/**
* Implements ArrayAccess
*/
public function offsetGet($name)
{
return $this->getChild($name);
}
/**
* Implements ArrayAccess
*/
public function offsetSet($name, $value)
{
return $this->addChild($name)->setLabel($value);
}
/**
* Implements ArrayAccess
*/
public function offsetUnset($name)
{
$this->removeChild($name);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Knp\Menu;
/**
* Interface implemented by a node to construct a menu from a tree.
*/
interface NodeInterface
{
/**
* Get the name of the node
*
* Each child of a node must have a unique name
*
* @return string
*/
public function getName();
/**
* Get the options for the factory to create the item for this node
*
* @return array
*/
public function getOptions();
/**
* Get the child nodes implementing NodeInterface
*
* @return \Traversable
*/
public function getChildren();
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Knp\Menu\Provider;
/**
* A menu provider getting the menus from a class implementing ArrayAccess.
*
* In case the value stored in the registry is a callable rather than an ItemInterface,
* it will be called with the options as first argument and the registry as second argument
* and is expected to return a menu item.
*/
class ArrayAccessProvider implements MenuProviderInterface
{
private $registry;
private $menuIds;
/**
* @param \ArrayAccess $registry
* @param array $menuIds The map between menu identifiers and registry keys
*/
public function __construct(\ArrayAccess $registry, array $menuIds = [])
{
$this->registry = $registry;
$this->menuIds = $menuIds;
}
public function get($name, array $options = [])
{
if (!isset($this->menuIds[$name])) {
throw new \InvalidArgumentException(\sprintf('The menu "%s" is not defined.', $name));
}
$menu = $this->registry[$this->menuIds[$name]];
if (\is_callable($menu)) {
$menu = \call_user_func($menu, $options, $this->registry);
}
return $menu;
}
public function has($name, array $options = [])
{
return isset($this->menuIds[$name]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Knp\Menu\Provider;
class ChainProvider implements MenuProviderInterface
{
private $providers;
/**
* @param MenuProviderInterface[]|iterable $providers
*/
public function __construct($providers)
{
$this->providers = $providers;
}
public function get($name, array $options = [])
{
foreach ($this->providers as $provider) {
if ($provider->has($name, $options)) {
return $provider->get($name, $options);
}
}
throw new \InvalidArgumentException(\sprintf('The menu "%s" is not defined.', $name));
}
public function has($name, array $options = [])
{
foreach ($this->providers as $provider) {
if ($provider->has($name, $options)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Knp\Menu\Provider;
/**
* A menu provider building menus lazily thanks to builder callables.
*
* Builders can either be callables or a factory for an object callable
* represented as array(Closure, method), where the Closure gets called
* to instantiate the object.
*/
class LazyProvider implements MenuProviderInterface
{
private $builders;
public function __construct(array $builders)
{
$this->builders = $builders;
}
public function get($name, array $options = [])
{
if (!isset($this->builders[$name])) {
throw new \InvalidArgumentException(\sprintf('The menu "%s" is not defined.', $name));
}
$builder = $this->builders[$name];
if (\is_array($builder) && isset($builder[0]) && $builder[0] instanceof \Closure) {
$builder[0] = $builder[0]();
}
if (!\is_callable($builder)) {
throw new \LogicException(\sprintf('Invalid menu builder for "%s". A callable or a factory for an object callable are expected.', $name));
}
return \call_user_func($builder, $options);
}
public function has($name, array $options = [])
{
return isset($this->builders[$name]);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Knp\Menu\Provider;
interface MenuProviderInterface
{
/**
* Retrieves a menu by its name
*
* @param string $name
* @param array $options
*
* @return \Knp\Menu\ItemInterface
*
* @throws \InvalidArgumentException if the menu does not exists
*/
public function get($name, array $options = []);
/**
* Checks whether a menu exists in this provider
*
* @param string $name
* @param array $options
*
* @return bool
*/
public function has($name, array $options = []);
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Knp\Menu\Provider;
@trigger_error('The '.__NAMESPACE__.'\PimpleProvider class is deprecated since version 2.1 and will be removed in 3.0. Use the '.__NAMESPACE__.'\ArrayAccessProvider class instead.', E_USER_DEPRECATED);
/**
* Menu provider getting menus from a Pimple 1 container
*
* @deprecated use the ArrayAccessProvider instead.
*/
class PimpleProvider extends ArrayAccessProvider
{
public function __construct(\Pimple $pimple, array $menuIds = [])
{
parent::__construct($pimple, $menuIds);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Knp\Menu\Provider;
use Psr\Container\ContainerInterface;
/**
* A menu provider getting the menus from a PSR-11 container.
*
* This menu provider does not support using options, as it cannot pass them to the container
* to alter the menu building. Use a different provider in case you need support for options.
*/
class PsrProvider implements MenuProviderInterface
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function get($name, array $options = [])
{
if (!$this->container->has($name)) {
throw new \InvalidArgumentException(\sprintf('The menu "%s" is not defined.', $name));
}
return $this->container->get($name);
}
public function has($name, array $options = [])
{
return $this->container->has($name);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Knp\Menu\Renderer;
/**
* A renderer provider getting the renderers from a class implementing ArrayAccess.
*/
class ArrayAccessProvider implements RendererProviderInterface
{
private $registry;
private $rendererIds;
private $defaultRenderer;
/**
* @param \ArrayAccess $registry
* @param string $defaultRenderer The name of the renderer used by default
* @param array $rendererIds The map between renderer names and regstry keys
*/
public function __construct(\ArrayAccess $registry, $defaultRenderer, array $rendererIds)
{
$this->registry = $registry;
$this->rendererIds = $rendererIds;
$this->defaultRenderer = $defaultRenderer;
}
public function get($name = null)
{
if (null === $name) {
$name = $this->defaultRenderer;
}
if (!isset($this->rendererIds[$name])) {
throw new \InvalidArgumentException(\sprintf('The renderer "%s" is not defined.', $name));
}
return $this->registry[$this->rendererIds[$name]];
}
public function has($name)
{
return isset($this->rendererIds[$name]);
}
}

View File

@@ -0,0 +1,250 @@
<?php
namespace Knp\Menu\Renderer;
use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\MatcherInterface;
/**
* Renders MenuItem tree as unordered list
*/
class ListRenderer extends Renderer implements RendererInterface
{
protected $matcher;
protected $defaultOptions;
/**
* @param MatcherInterface $matcher
* @param array $defaultOptions
* @param string|null $charset
*/
public function __construct(MatcherInterface $matcher, array $defaultOptions = [], $charset = null)
{
$this->matcher = $matcher;
$this->defaultOptions = \array_merge([
'depth' => null,
'matchingDepth' => null,
'currentAsLink' => true,
'currentClass' => 'current',
'ancestorClass' => 'current_ancestor',
'firstClass' => 'first',
'lastClass' => 'last',
'compressed' => false,
'allow_safe_labels' => false,
'clear_matcher' => true,
'leaf_class' => null,
'branch_class' => null,
], $defaultOptions);
parent::__construct($charset);
}
public function render(ItemInterface $item, array $options = [])
{
$options = \array_merge($this->defaultOptions, $options);
$html = $this->renderList($item, $item->getChildrenAttributes(), $options);
if ($options['clear_matcher']) {
$this->matcher->clear();
}
return $html;
}
protected function renderList(ItemInterface $item, array $attributes, array $options)
{
/**
* Return an empty string if any of the following are true:
* a) The menu has no children eligible to be displayed
* b) The depth is 0
* c) This menu item has been explicitly set to hide its children
*/
if (!$item->hasChildren() || 0 === $options['depth'] || !$item->getDisplayChildren()) {
return '';
}
$html = $this->format('<ul'.$this->renderHtmlAttributes($attributes).'>', 'ul', $item->getLevel(), $options);
$html .= $this->renderChildren($item, $options);
$html .= $this->format('</ul>', 'ul', $item->getLevel(), $options);
return $html;
}
/**
* Renders all of the children of this menu.
*
* This calls ->renderItem() on each menu item, which instructs each
* menu item to render themselves as an <li> tag (with nested ul if it
* has children).
* This method updates the depth for the children.
*
* @param ItemInterface $item
* @param array $options The options to render the item.
*
* @return string
*/
protected function renderChildren(ItemInterface $item, array $options)
{
// render children with a depth - 1
if (null !== $options['depth']) {
$options['depth'] = $options['depth'] - 1;
}
if (null !== $options['matchingDepth'] && $options['matchingDepth'] > 0) {
$options['matchingDepth'] = $options['matchingDepth'] - 1;
}
$html = '';
foreach ($item->getChildren() as $child) {
$html .= $this->renderItem($child, $options);
}
return $html;
}
/**
* Called by the parent menu item to render this menu.
*
* This renders the li tag to fit into the parent ul as well as its
* own nested ul tag if this menu item has children
*
* @param ItemInterface $item
* @param array $options The options to render the item
*
* @return string
*/
protected function renderItem(ItemInterface $item, array $options)
{
// if we don't have access or this item is marked to not be shown
if (!$item->isDisplayed()) {
return '';
}
// create an array than can be imploded as a class list
$class = (array) $item->getAttribute('class');
if ($this->matcher->isCurrent($item)) {
$class[] = $options['currentClass'];
} elseif ($this->matcher->isAncestor($item, $options['matchingDepth'])) {
$class[] = $options['ancestorClass'];
}
if ($item->actsLikeFirst()) {
$class[] = $options['firstClass'];
}
if ($item->actsLikeLast()) {
$class[] = $options['lastClass'];
}
if ($item->hasChildren() && 0 !== $options['depth']) {
if (null !== $options['branch_class'] && $item->getDisplayChildren()) {
$class[] = $options['branch_class'];
}
} elseif (null !== $options['leaf_class']) {
$class[] = $options['leaf_class'];
}
// retrieve the attributes and put the final class string back on it
$attributes = $item->getAttributes();
if (!empty($class)) {
$attributes['class'] = \implode(' ', $class);
}
// opening li tag
$html = $this->format('<li'.$this->renderHtmlAttributes($attributes).'>', 'li', $item->getLevel(), $options);
// render the text/link inside the li tag
//$html .= $this->format($item->getUri() ? $item->renderLink() : $item->renderLabel(), 'link', $item->getLevel());
$html .= $this->renderLink($item, $options);
// renders the embedded ul
$childrenClass = (array) $item->getChildrenAttribute('class');
$childrenClass[] = 'menu_level_'.$item->getLevel();
$childrenAttributes = $item->getChildrenAttributes();
$childrenAttributes['class'] = \implode(' ', $childrenClass);
$html .= $this->renderList($item, $childrenAttributes, $options);
// closing li tag
$html .= $this->format('</li>', 'li', $item->getLevel(), $options);
return $html;
}
/**
* Renders the link in a a tag with link attributes or
* the label in a span tag with label attributes
*
* Tests if item has a an uri and if not tests if it's
* the current item and if the text has to be rendered
* as a link or not.
*
* @param ItemInterface $item The item to render the link or label for
* @param array $options The options to render the item
*
* @return string
*/
protected function renderLink(ItemInterface $item, array $options = [])
{
if ($item->getUri() && (!$item->isCurrent() || $options['currentAsLink'])) {
$text = $this->renderLinkElement($item, $options);
} else {
$text = $this->renderSpanElement($item, $options);
}
return $this->format($text, 'link', $item->getLevel(), $options);
}
protected function renderLinkElement(ItemInterface $item, array $options)
{
return \sprintf('<a href="%s"%s>%s</a>', $this->escape($item->getUri()), $this->renderHtmlAttributes($item->getLinkAttributes()), $this->renderLabel($item, $options));
}
protected function renderSpanElement(ItemInterface $item, array $options)
{
return \sprintf('<span%s>%s</span>', $this->renderHtmlAttributes($item->getLabelAttributes()), $this->renderLabel($item, $options));
}
protected function renderLabel(ItemInterface $item, array $options)
{
if ($options['allow_safe_labels'] && $item->getExtra('safe_label', false)) {
return $item->getLabel();
}
return $this->escape($item->getLabel());
}
/**
* If $this->renderCompressed is on, this will apply the necessary
* spacing and line-breaking so that the particular thing being rendered
* makes up its part in a fully-rendered and spaced menu.
*
* @param string $html The html to render in an (un)formatted way
* @param string $type The type [ul,link,li] of thing being rendered
* @param int $level
* @param array $options
*
* @return string
*/
protected function format($html, $type, $level, array $options)
{
if ($options['compressed']) {
return $html;
}
switch ($type) {
case 'ul':
case 'link':
$spacing = $level * 4;
break;
case 'li':
$spacing = $level * 4 - 2;
break;
}
return \str_repeat(' ', $spacing).$html."\n";
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Knp\Menu\Renderer;
@trigger_error('The '.__NAMESPACE__.'\PimpleProvider class is deprecated since version 2.1 and will be removed in 3.0. Use the '.__NAMESPACE__.'\ArrayAccessProvider class instead.', E_USER_DEPRECATED);
/**
* Renderer provider getting renderers from a Pimple 1 container
*
* @deprecated use the ArrayAccessProvider instead.
*/
class PimpleProvider extends ArrayAccessProvider
{
public function __construct(\Pimple $pimple, $defaultRenderer, array $rendererIds)
{
parent::__construct($pimple, $defaultRenderer, $rendererIds);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Knp\Menu\Renderer;
use Psr\Container\ContainerInterface;
/**
* A renderer provider getting the renderer from a PSR-11 container.
*
* This menu provider does not support using options, as it cannot pass them to the container
* to alter the menu building. Use a different provider in case you need support for options.
*/
class PsrProvider implements RendererProviderInterface
{
private $container;
private $defaultRenderer;
/**
* PsrProvider constructor.
*
* @param ContainerInterface $container
* @param string $defaultRenderer id of the default renderer (it should exist in the container to avoid weird failures)
*/
public function __construct(ContainerInterface $container, $defaultRenderer)
{
$this->container = $container;
$this->defaultRenderer = $defaultRenderer;
}
public function get($name = null)
{
if (null === $name) {
$name = $this->defaultRenderer;
}
if (!$this->container->has($name)) {
throw new \InvalidArgumentException(\sprintf('The renderer "%s" is not defined.', $name));
}
return $this->container->get($name);
}
public function has($name)
{
return $this->container->has($name);
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Knp\Menu\Renderer;
abstract class Renderer
{
protected $charset = 'UTF-8';
/**
* @param string|null $charset
*/
public function __construct($charset = null)
{
if (null !== $charset) {
$this->charset = (string) $charset;
}
}
/**
* Renders a HTML attribute
*
* @param string $name
* @param string|bool $value
*
* @return string
*/
protected function renderHtmlAttribute($name, $value)
{
if (true === $value) {
return \sprintf('%s="%s"', $name, $this->escape($name));
}
return \sprintf('%s="%s"', $name, $this->escape($value));
}
/**
* Renders HTML attributes
*
* @param array $attributes
*
* @return string
*/
protected function renderHtmlAttributes(array $attributes)
{
return \implode('', \array_map([$this, 'htmlAttributesCallback'], \array_keys($attributes), \array_values($attributes)));
}
/**
* Prepares an attribute key and value for HTML representation.
*
* It removes empty attributes.
*
* @param string $name The attribute name
* @param string|bool|null $value The attribute value
*
* @return string The HTML representation of the HTML key attribute pair.
*/
private function htmlAttributesCallback($name, $value)
{
if (false === $value || null === $value) {
return '';
}
return ' '.$this->renderHtmlAttribute($name, $value);
}
/**
* Escapes an HTML value
*
* @param string $value
*
* @return string
*/
protected function escape($value)
{
return $this->fixDoubleEscape(\htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, $this->charset));
}
/**
* Fixes double escaped strings.
*
* @param string $escaped string to fix
*
* @return string A single escaped string
*/
protected function fixDoubleEscape($escaped)
{
return \preg_replace('/&amp;([a-z]+|(#\d+)|(#x[\da-f]+));/i', '&$1;', $escaped);
}
/**
* Get the HTML charset
*
* @return string
*/
public function getCharset()
{
return $this->charset;
}
/**
* Set the HTML charset
*
* @param string $charset
*/
public function setCharset($charset)
{
$this->charset = (string) $charset;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Knp\Menu\Renderer;
use Knp\Menu\ItemInterface;
interface RendererInterface
{
/**
* Renders menu tree.
*
* Common options:
* - depth: The depth at which the item is rendered
* null: no limit
* 0: no children
* 1: only direct children
* - currentAsLink: whether the current item should be a link
* - currentClass: class added to the current item
* - ancestorClass: class added to the ancestors of the current item
* - firstClass: class added to the first child
* - lastClass: class added to the last child
*
* @param ItemInterface $item Menu item
* @param array $options some rendering options
*
* @return string
*/
public function render(ItemInterface $item, array $options = []);
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Knp\Menu\Renderer;
interface RendererProviderInterface
{
/**
* Retrieves a renderer by its name
*
* If null is given, a renderer marked as default is returned.
*
* @param string|null $name
*
* @return RendererInterface
*
* @throws \InvalidArgumentException if the renderer does not exists
*/
public function get($name = null);
/**
* Checks whether a renderer exists
*
* @param string $name
*
* @return bool
*/
public function has($name);
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Knp\Menu\Renderer;
use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\MatcherInterface;
use Twig\Environment;
class TwigRenderer implements RendererInterface
{
/**
* @var Environment
*/
private $environment;
private $matcher;
private $defaultOptions;
/**
* @param Environment $environment
* @param string $template
* @param MatcherInterface $matcher
* @param array $defaultOptions
*/
public function __construct(Environment $environment, $template, MatcherInterface $matcher, array $defaultOptions = [])
{
$this->environment = $environment;
$this->matcher = $matcher;
$this->defaultOptions = \array_merge([
'depth' => null,
'matchingDepth' => null,
'currentAsLink' => true,
'currentClass' => 'current',
'ancestorClass' => 'current_ancestor',
'firstClass' => 'first',
'lastClass' => 'last',
'template' => $template,
'compressed' => false,
'allow_safe_labels' => false,
'clear_matcher' => true,
'leaf_class' => null,
'branch_class' => null,
], $defaultOptions);
}
public function render(ItemInterface $item, array $options = [])
{
$options = \array_merge($this->defaultOptions, $options);
$html = $this->environment->render($options['template'], ['item' => $item, 'options' => $options, 'matcher' => $this->matcher]);
if ($options['clear_matcher']) {
$this->matcher->clear();
}
return $html;
}
}

View File

@@ -0,0 +1,101 @@
{% extends 'knp_menu_base.html.twig' %}
{% macro attributes(attributes) %}
{% for name, value in attributes %}
{%- if value is not none and value is not same as(false) -%}
{{- ' %s="%s"'|format(name, value is same as(true) ? name|e : value|e)|raw -}}
{%- endif -%}
{%- endfor -%}
{% endmacro %}
{% block compressed_root %}
{% apply spaceless %}
{{ block('root') }}
{% endapply %}
{% endblock %}
{% block root %}
{% set listAttributes = item.childrenAttributes %}
{{ block('list') -}}
{% endblock %}
{% block list %}
{% if item.hasChildren and options.depth is not same as(0) and item.displayChildren %}
{% import _self as knp_menu %}
<ul{{ knp_menu.attributes(listAttributes) }}>
{{ block('children') }}
</ul>
{% endif %}
{% endblock %}
{% block children %}
{# save current variables #}
{% set currentOptions = options %}
{% set currentItem = item %}
{# update the depth for children #}
{% if options.depth is not none %}
{% set options = options|merge({'depth': currentOptions.depth - 1}) %}
{% endif %}
{# update the matchingDepth for children #}
{% if options.matchingDepth is not none and options.matchingDepth > 0 %}
{% set options = options|merge({'matchingDepth': currentOptions.matchingDepth - 1}) %}
{% endif %}
{% for item in currentItem.children %}
{{ block('item') }}
{% endfor %}
{# restore current variables #}
{% set item = currentItem %}
{% set options = currentOptions %}
{% endblock %}
{% block item %}
{% if item.displayed %}
{# building the class of the item #}
{%- set classes = item.attribute('class') is not empty ? [item.attribute('class')] : [] %}
{%- if matcher.isCurrent(item) %}
{%- set classes = classes|merge([options.currentClass]) %}
{%- elseif matcher.isAncestor(item, options.matchingDepth) %}
{%- set classes = classes|merge([options.ancestorClass]) %}
{%- endif %}
{%- if item.actsLikeFirst %}
{%- set classes = classes|merge([options.firstClass]) %}
{%- endif %}
{%- if item.actsLikeLast %}
{%- set classes = classes|merge([options.lastClass]) %}
{%- endif %}
{# Mark item as "leaf" (no children) or as "branch" (has children that are displayed) #}
{% if item.hasChildren and options.depth is not same as(0) %}
{% if options.branch_class is not empty and item.displayChildren %}
{%- set classes = classes|merge([options.branch_class]) %}
{% endif %}
{% elseif options.leaf_class is not empty %}
{%- set classes = classes|merge([options.leaf_class]) %}
{%- endif %}
{%- set attributes = item.attributes %}
{%- if classes is not empty %}
{%- set attributes = attributes|merge({'class': classes|join(' ')}) %}
{%- endif %}
{# displaying the item #}
{% import _self as knp_menu %}
<li{{ knp_menu.attributes(attributes) }}>
{%- if item.uri is not empty and (not matcher.isCurrent(item) or options.currentAsLink) %}
{{ block('linkElement') }}
{%- else %}
{{ block('spanElement') }}
{%- endif %}
{# render the list of children#}
{%- set childrenClasses = item.childrenAttribute('class') is not empty ? [item.childrenAttribute('class')] : [] %}
{%- set childrenClasses = childrenClasses|merge(['menu_level_' ~ item.level]) %}
{%- set listAttributes = item.childrenAttributes|merge({'class': childrenClasses|join(' ') }) %}
{{ block('list') }}
</li>
{% endif %}
{% endblock %}
{% block linkElement %}{% import _self as knp_menu %}<a href="{{ item.uri }}"{{ knp_menu.attributes(item.linkAttributes) }}>{{ block('label') }}</a>{% endblock %}
{% block spanElement %}{% import _self as knp_menu %}<span{{ knp_menu.attributes(item.labelAttributes) }}>{{ block('label') }}</span>{% endblock %}
{% block label %}{% if options.allow_safe_labels and item.getExtra('safe_label', false) %}{{ item.label|raw }}{% else %}{{ item.label }}{% endif %}{% endblock %}

View File

@@ -0,0 +1 @@
{% if options.compressed %}{{ block('compressed_root') }}{% else %}{{ block('root') }}{% endif %}

View File

@@ -0,0 +1,11 @@
{% extends 'knp_menu.html.twig' %}
{% block list %}
{% import 'knp_menu.html.twig' as macros %}
{% if item.hasChildren and options.depth is not same as(0) and item.displayChildren %}
<ol{{ macros.attributes(listAttributes) }}>
{{ block('children') }}
</ol>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,191 @@
<?php
namespace Knp\Menu\Twig;
use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\MatcherInterface;
use Knp\Menu\Provider\MenuProviderInterface;
use Knp\Menu\Renderer\RendererProviderInterface;
use Knp\Menu\Util\MenuManipulator;
/**
* Helper class containing logic to retrieve and render menus from templating engines
*/
class Helper
{
private $rendererProvider;
private $menuProvider;
private $menuManipulator;
private $matcher;
/**
* @param RendererProviderInterface $rendererProvider
* @param MenuProviderInterface|null $menuProvider
* @param MenuManipulator|null $menuManipulator
* @param MatcherInterface|null $matcher
*/
public function __construct(RendererProviderInterface $rendererProvider, MenuProviderInterface $menuProvider = null, MenuManipulator $menuManipulator = null, MatcherInterface $matcher = null)
{
$this->rendererProvider = $rendererProvider;
$this->menuProvider = $menuProvider;
$this->menuManipulator = $menuManipulator;
$this->matcher = $matcher;
}
/**
* Retrieves item in the menu, eventually using the menu provider.
*
* @param ItemInterface|string $menu
* @param array $path
* @param array $options
*
* @return ItemInterface
*
* @throws \BadMethodCallException when there is no menu provider and the menu is given by name
* @throws \LogicException
* @throws \InvalidArgumentException when the path is invalid
*/
public function get($menu, array $path = [], array $options = [])
{
if (!$menu instanceof ItemInterface) {
if (null === $this->menuProvider) {
throw new \BadMethodCallException('A menu provider must be set to retrieve a menu');
}
$menuName = $menu;
$menu = $this->menuProvider->get($menuName, $options);
if (!$menu instanceof ItemInterface) {
throw new \LogicException(\sprintf('The menu "%s" exists, but is not a valid menu item object. Check where you created the menu to be sure it returns an ItemInterface object.', $menuName));
}
}
foreach ($path as $child) {
$menu = $menu->getChild($child);
if (null === $menu) {
throw new \InvalidArgumentException(\sprintf('The menu has no child named "%s"', $child));
}
}
return $menu;
}
/**
* Renders a menu with the specified renderer.
*
* If the argument is an array, it will follow the path in the tree to
* get the needed item. The first element of the array is the whole menu.
* If the menu is a string instead of an ItemInterface, the provider
* will be used.
*
* @param ItemInterface|string|array $menu
* @param array $options
* @param string $renderer
*
* @return string
*
* @throws \InvalidArgumentException
*/
public function render($menu, array $options = [], $renderer = null)
{
$menu = $this->castMenu($menu);
return $this->rendererProvider->get($renderer)->render($menu, $options);
}
/**
* Renders an array ready to be used for breadcrumbs.
*
* Each element in the array will be an array with 3 keys:
* - `label` containing the label of the item
* - `url` containing the url of the item (may be `null`)
* - `item` containing the original item (may be `null` for the extra items)
*
* The subItem can be one of the following forms
* * 'subItem'
* * ItemInterface object
* * ['subItem' => '@homepage']
* * ['subItem1', 'subItem2']
* * [['label' => 'subItem1', 'url' => '@homepage'], ['label' => 'subItem2']]
*
* @param mixed $menu
* @param mixed $subItem A string or array to append onto the end of the array
*
* @return array
*/
public function getBreadcrumbsArray($menu, $subItem = null)
{
if (null === $this->menuManipulator) {
throw new \BadMethodCallException('The menu manipulator must be set to get the breadcrumbs array');
}
$menu = $this->castMenu($menu);
return $this->menuManipulator->getBreadcrumbsArray($menu, $subItem);
}
/**
* Returns the current item of a menu.
*
* @param ItemInterface|array|string $menu
*
* @return ItemInterface|null
*/
public function getCurrentItem($menu)
{
if (null === $this->matcher) {
throw new \BadMethodCallException('The matcher must be set to get the current item of a menu');
}
$menu = $this->castMenu($menu);
return $this->retrieveCurrentItem($menu);
}
/**
* @param ItemInterface|array|string $menu
*
* @return ItemInterface
*/
private function castMenu($menu)
{
if (!$menu instanceof ItemInterface) {
$path = [];
if (\is_array($menu)) {
if (empty($menu)) {
throw new \InvalidArgumentException('The array cannot be empty');
}
$path = $menu;
$menu = \array_shift($path);
}
return $this->get($menu, $path);
}
return $menu;
}
/**
* @param ItemInterface $item
*
* @return ItemInterface|null
*/
private function retrieveCurrentItem(ItemInterface $item)
{
if ($this->matcher->isCurrent($item)) {
return $item;
}
if ($this->matcher->isAncestor($item)) {
foreach ($item->getChildren() as $child) {
$currentItem = $this->retrieveCurrentItem($child);
if (null !== $currentItem) {
return $currentItem;
}
}
}
return null;
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace Knp\Menu\Twig;
use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\MatcherInterface;
use Knp\Menu\Util\MenuManipulator;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
class MenuExtension extends AbstractExtension
{
private $helper;
private $matcher;
private $menuManipulator;
public function __construct(Helper $helper, MatcherInterface $matcher = null, MenuManipulator $menuManipulator = null)
{
$this->helper = $helper;
$this->matcher = $matcher;
$this->menuManipulator = $menuManipulator;
}
public function getFunctions()
{
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']),
];
}
public function getFilters()
{
return [
new TwigFilter('knp_menu_as_string', [$this, 'pathAsString']),
];
}
public function getTests()
{
return [
new TwigTest('knp_menu_current', [$this, 'isCurrent']),
new TwigTest('knp_menu_ancestor', [$this, 'isAncestor']),
];
}
/**
* Retrieves an item following a path in the tree.
*
* @param ItemInterface|string $menu
* @param array $path
* @param array $options
*
* @return ItemInterface
*/
public function get($menu, array $path = [], array $options = [])
{
return $this->helper->get($menu, $path, $options);
}
/**
* Renders a menu with the specified renderer.
*
* @param ItemInterface|string|array $menu
* @param array $options
* @param string $renderer
*
* @return string
*/
public function render($menu, array $options = [], $renderer = null)
{
return $this->helper->render($menu, $options, $renderer);
}
/**
* Returns an array ready to be used for breadcrumbs.
*
* @param ItemInterface|array|string $menu
* @param string|array|null $subItem
*
* @return array
*/
public function getBreadcrumbsArray($menu, $subItem = null)
{
return $this->helper->getBreadcrumbsArray($menu, $subItem);
}
/**
* Returns the current item of a menu.
*
* @param ItemInterface|string $menu
*
* @return ItemInterface
*/
public function getCurrentItem($menu)
{
$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
*
* @param ItemInterface $menu
* @param string $separator
*
* @return string
*/
public function pathAsString(ItemInterface $menu, $separator = ' > ')
{
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.
*
* @param ItemInterface $item
*
* @return bool
*/
public function isCurrent(ItemInterface $item)
{
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 ItemInterface $item
* @param int|null $depth The max depth to look for the item
*
* @return bool
*/
public function isAncestor(ItemInterface $item, $depth = null)
{
if (null === $this->matcher) {
throw new \BadMethodCallException('The matcher must be set to get the breadcrumbs array');
}
return $this->matcher->isAncestor($item, $depth);
}
/**
* @return string
*/
public function getName()
{
return 'knp_menu';
}
}

View File

@@ -0,0 +1,306 @@
<?php
namespace Knp\Menu\Util;
use Knp\Menu\ItemInterface;
class MenuManipulator
{
/**
* Moves item to specified position. Rearrange siblings accordingly.
*
* @param ItemInterface $item
* @param int $position Position to move child to.
*/
public function moveToPosition(ItemInterface $item, $position)
{
$this->moveChildToPosition($item->getParent(), $item, $position);
}
/**
* Moves child to specified position. Rearrange other children accordingly.
*
* @param ItemInterface $item
* @param ItemInterface $child Child to move
* @param int $position Position to move child to
*/
public function moveChildToPosition(ItemInterface $item, ItemInterface $child, $position)
{
$name = $child->getName();
$order = \array_keys($item->getChildren());
$oldPosition = \array_search($name, $order);
unset($order[$oldPosition]);
$order = \array_values($order);
\array_splice($order, $position, 0, $name);
$item->reorderChildren($order);
}
/**
* Moves item to first position. Rearrange siblings accordingly.
*
* @param ItemInterface $item
*/
public function moveToFirstPosition(ItemInterface $item)
{
$this->moveToPosition($item, 0);
}
/**
* Moves item to last position. Rearrange siblings accordingly.
*
* @param ItemInterface $item
*/
public function moveToLastPosition(ItemInterface $item)
{
$this->moveToPosition($item, $item->getParent()->count());
}
/**
* Get slice of menu as another menu.
*
* If offset and/or length are numeric, it works like in array_slice function:
*
* If offset is non-negative, slice will start at the offset.
* If offset is negative, slice will start that far from the end.
*
* If length is null, slice will have all elements.
* If length is positive, slice will have that many elements.
* If length is negative, slice will stop that far from the end.
*
* It's possible to mix names/object/numeric, for example:
* slice("child1", 2);
* slice(3, $child5);
* Note: when using a child as limit, it will not be included in the returned menu.
* the slice is done before this menu.
*
* @param ItemInterface $item
* @param mixed $offset Name of child, child object, or numeric offset.
* @param mixed $length Name of child, child object, or numeric length.
*
* @return ItemInterface
*/
public function slice(ItemInterface $item, $offset, $length = null)
{
$names = \array_keys($item->getChildren());
if ($offset instanceof ItemInterface) {
$offset = $offset->getName();
}
if (!\is_numeric($offset)) {
$offset = \array_search($offset, $names);
}
if (null !== $length) {
if ($length instanceof ItemInterface) {
$length = $length->getName();
}
if (!\is_numeric($length)) {
$index = \array_search($length, $names);
$length = ($index < $offset) ? 0 : $index - $offset;
}
}
$slicedItem = $item->copy();
$children = \array_slice($slicedItem->getChildren(), $offset, $length);
$slicedItem->setChildren($children);
return $slicedItem;
}
/**
* Split menu into two distinct menus.
*
* @param ItemInterface $item
* @param mixed $length Name of child, child object, or numeric length.
*
* @return array Array with two menus, with "primary" and "secondary" key
*/
public function split(ItemInterface $item, $length)
{
return [
'primary' => $this->slice($item, 0, $length),
'secondary' => $this->slice($item, $length),
];
}
/**
* Calls a method recursively on all of the children of this item
*
* @example
* $menu->callRecursively('setShowChildren', array(false));
*
* @param ItemInterface $item
* @param string $method
* @param array $arguments
*/
public function callRecursively(ItemInterface $item, $method, $arguments = [])
{
\call_user_func_array([$item, $method], $arguments);
foreach ($item->getChildren() as $child) {
$this->callRecursively($child, $method, $arguments);
}
}
/**
* A string representation of this menu item
*
* e.g. Top Level > Second Level > This menu
*
* @param ItemInterface $item
* @param string $separator
*
* @return string
*/
public function getPathAsString(ItemInterface $item, $separator = ' > ')
{
$children = [];
$obj = $item;
do {
$children[] = $obj->getLabel();
} while ($obj = $obj->getParent());
return \implode($separator, \array_reverse($children));
}
/**
* @param ItemInterface $item
* @param int|null $depth the depth until which children should be exported (null means unlimited)
*
* @return array
*/
public function toArray(ItemInterface $item, $depth = null)
{
$array = [
'name' => $item->getName(),
'label' => $item->getLabel(),
'uri' => $item->getUri(),
'attributes' => $item->getAttributes(),
'labelAttributes' => $item->getLabelAttributes(),
'linkAttributes' => $item->getLinkAttributes(),
'childrenAttributes' => $item->getChildrenAttributes(),
'extras' => $item->getExtras(),
'display' => $item->isDisplayed(),
'displayChildren' => $item->getDisplayChildren(),
'current' => $item->isCurrent(),
];
// export the children as well, unless explicitly disabled
if (0 !== $depth) {
$childDepth = null === $depth ? null : $depth - 1;
$array['children'] = [];
foreach ($item->getChildren() as $key => $child) {
$array['children'][$key] = $this->toArray($child, $childDepth);
}
}
return $array;
}
/**
* Renders an array ready to be used for breadcrumbs.
*
* Each element in the array will be an array with 3 keys:
* - `label` containing the label of the item
* - `url` containing the url of the item (may be `null`)
* - `item` containing the original item (may be `null` for the extra items)
*
* The subItem can be one of the following forms
* * 'subItem'
* * ItemInterface object
* * array('subItem' => '@homepage')
* * array('subItem1', 'subItem2')
* * array(array('label' => 'subItem1', 'url' => '@homepage'), array('label' => 'subItem2'))
*
* @param ItemInterface $item
* @param mixed $subItem A string or array to append onto the end of the array
*
* @return array
*
* @throws \InvalidArgumentException if an element of the subItem is invalid
*/
public function getBreadcrumbsArray(ItemInterface $item, $subItem = null)
{
$breadcrumbs = $this->buildBreadcrumbsArray($item);
if (null === $subItem) {
return $breadcrumbs;
}
if ($subItem instanceof ItemInterface) {
$breadcrumbs[] = $this->getBreadcrumbsItem($subItem);
return $breadcrumbs;
}
if (!\is_array($subItem) && !$subItem instanceof \Traversable) {
$subItem = [$subItem];
}
foreach ($subItem as $key => $value) {
switch (true) {
case $value instanceof ItemInterface:
$value = $this->getBreadcrumbsItem($value);
break;
case \is_array($value):
// Assume we already have the appropriate array format for the element
break;
case \is_int($key) && \is_string($value):
$value = [
'label' => (string) $value,
'uri' => null,
'item' => null,
];
break;
case \is_scalar($value):
$value = [
'label' => (string) $key,
'uri' => (string) $value,
'item' => null,
];
break;
case null === $value:
$value = [
'label' => (string) $key,
'uri' => null,
'item' => null,
];
break;
default:
throw new \InvalidArgumentException(\sprintf('Invalid value supplied for the key "%s". It should be an item, an array or a scalar', $key));
}
$breadcrumbs[] = $value;
}
return $breadcrumbs;
}
private function buildBreadcrumbsArray(ItemInterface $item)
{
$breadcrumb = [];
do {
$breadcrumb[] = $this->getBreadcrumbsItem($item);
} while ($item = $item->getParent());
return \array_reverse($breadcrumb);
}
private function getBreadcrumbsItem(ItemInterface $item)
{
return [
'label' => $item->getLabel(),
'uri' => $item->getUri(),
'item' => $item,
];
}
}