mimmi20 / mezzio-navigation
Provides a Navigation for Mezzio.
Installs: 176 045
Dependents: 11
Suggesters: 7
Security: 0
Stars: 4
Watchers: 3
Forks: 1
Open Issues: 1
Requires
- php: ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
- ext-mbstring: *
- laminas/laminas-stdlib: ^3.19.0
- mimmi20/mezzio-generic-authorization: ^3.0.6
- psr/container: ^1.1.2 || ^2.0.2
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0.2
- psr/http-server-middleware: ^1.0.2
Requires (Dev)
- ext-ctype: *
- ext-dom: *
- ext-simplexml: *
- ext-tokenizer: *
- ext-xml: *
- ext-xmlwriter: *
- infection/infection: ^0.27.11 || ^0.28.1
- laminas/laminas-config: ^3.9.0
- laminas/laminas-servicemanager: ^3.22.1 || ^4.0.0
- mezzio/mezzio-helpers: ^5.16.0
- mezzio/mezzio-router: ^3.17.0
- mimmi20/coding-standard: ^5.2.43
- nikic/php-parser: ^4.19.1 || ^5.0.2
- phpstan/extension-installer: ^1.4.3
- phpstan/phpstan: ^1.12.3
- phpstan/phpstan-deprecation-rules: ^1.2.0
- phpstan/phpstan-phpunit: ^1.4.0
- phpunit/phpunit: ^10.5.26
- rector/rector: ^1.2.5
- rector/type-perfect: ^0.2.0
- symplify/phpstan-rules: ^13.0.1
- tomasvotruba/cognitive-complexity: ^0.2.3
- tomasvotruba/type-coverage: ^0.3.1
- tomasvotruba/unused-public: ^0.3.11
Suggests
- laminas/laminas-config: to provide page configuration (optional, as arrays and Traversables are also allowed)
- laminas/laminas-servicemanager: to use the navigation factories
- mezzio/mezzio-router: to use router-based URI generation with pages
- mimmi20/mezzio-generic-authorization: to provide access restrictions to pages
- mimmi20/mezzio-navigation-laminasviewrenderer: to use the navigation view helpers
Conflicts
README
Code Status
Introduction
This component provides a component for managing trees of pointers to web pages. Simply put: It can be used for creating menus, breadcrumbs, links, and sitemaps, or serve as a model for other navigation related purposes.
Unlike in laminas-navigation this library provides a middleware which is required to prepare the navigation.
Installation
You can install the mezzio-navigation library with Composer:
composer require mimmi20/mezzio-navigation
Pages and Containers
There are two main concepts in mezzio-navigation: pages and containers.
Pages
A page in mezzio-navigation, in its most basic
form, is an object that holds a pointer to a web page. In addition to the
pointer itself, the page object contains a number of other properties that are
typically relevant for navigation, such as label
, title
, etc.
Read more about pages in the pages section.
Containers
A navigation container holds pages. It has
methods for adding, retrieving, deleting and iterating pages. It implements the
SPL interfaces RecursiveIterator
and Countable
, and
can thus be iterated with SPL iterators such as RecursiveIteratorIterator
.
Read more about containers in the containers section.
Pages are containers
Mezzio\Navigation\PageInterface
extendsMezzio\Navigation\ContainerInterface
, which means that a page can have sub pages.
View Helpers
Separation of data (model) and rendering (view)
Classes in the mezzio-navigation namespace do not deal with rendering of
navigational elements. Rendering is done with navigational view helpers.
However, pages contain information that is used by view helpers when rendering,
such as label
, class
(CSS), title
, lastmod
, and priority
properties
for sitemaps, etc.
We provide one rendering libary.
- The Renderer using Laminas View is provided by mezzio-navigation-laminasviewrenderer.
The renderer is installable via Composer:
composer require mimmi20/mezzio-navigation-laminasviewrenderer
Containers
Containers have methods for adding, retrieving, deleting, and iterating pages.
Containers implement the SPL interfaces
RecursiveIterator
and Countable
, meaning that a container can be iterated
using the SPL RecursiveIteratorIterator
class.
Creating containers
Mezzio\Navigation\ContainerInterface
can not be instantiated directly. Use
Mezzio\Navigation\Navigation
if you want to instantiate a container.
Mezzio\Navigation\Navigation
can be constructed entirely empty, or take an array
or a Traversable
object with pages to put in the container. Each page provided
via options will eventually be passed to the addPage()
method of the container
class, which means that each element in the options can be also be an array,
Traversable object, or a Mezzio\Navigation\Page\PageInterface
instance.
Creating a container using an array
Unlike in Laminas Navigation it is not possible to add an arrray config to Mezzio Navigation directly. Converting an array config into a list of Page objects is made in the Navigation Factories.
use Mimmi20\Mezzio\Navigation\Navigation; use Mimmi20\Mezzio\Navigation\Config\NavigationConfigInterface; /* * Create a container from an array */ $navigationConfig = $serviceLocator->get(NavigationConfigInterface::class); $navigationConfig->setPages([ [ 'label' => 'Page 1', 'id' => 'home-link', 'uri' => '/', ], [ 'label' => 'Laminas', 'uri' => 'http://www.laminas-project.com/', 'order' => 100, ], [ 'label' => 'Page 2', 'route' => 'page2', ], ]); $container = $serviceLocator->get(Navigation::class);
Adding pages
Adding pages to a container can be done with the methods addPage()
,
addPages()
, or setPages()
. See examples below for explanation.
Unlike in Laminas Navigation only objects implementing the PageInterface are allowed. The PageFactory may be used to use an array config.
use Laminas\Config\Config; use Mimmi20\Mezzio\Navigation\Navigation; use Mimmi20\Mezzio\Navigation\Page\PageFactory; // create container $container = new Navigation(); // add page by giving a page instance $container->addPage(new Class implements PageInterface{}); // add page by giving a page instance using the PageFactory $container->addPage((new PageFactory())->factory([ 'uri' => 'http://www.example.com/', ])); $pages = [ new Class implements PageInterface{}, new Class implements PageInterface{}, ]; // add two pages $container->addPages($pages); // remove existing pages and add the given pages $container->setPages($pages);
Removing pages
Removing pages can be done with removePage()
or removePages()
.
removePage()
accepts an instance of a page or an integer. Integer arguments
correspond to the order
a page has. removePages()
will remove all pages in
the container.
use Mimmi20\Mezzio\Navigation\Navigation; $navigationConfig = $serviceLocator->get(NavigationConfigInterface::class); $navigationConfig->setPages([ [ 'label' => 'Page 1', 'action' => 'page1', ], [ 'label' => 'Page 2', 'action' => 'page2', 'order' => 200, ], [ 'label' => 'Page 3', 'action' => 'page3', ], ]); $container = $serviceLocator->get(Navigation::class); // remove page by implicit page order $container->removePage(0); // removes Page 1 // remove page by instance $page3 = $container->findOneByAction('page3'); $container->removePage($page3); // removes Page 3 // remove page by explicit page order $container->removePage(200); // removes Page 2 // remove all pages $container->removePages(); // removes all pages
Remove a page recursively
Removing a page recursively can be done with the second parameter of
the removePage()
method, which expects a boolean
value.
use Mimmi20\Mezzio\Navigation\Navigation; $navigationConfig = $serviceLocator->get(NavigationConfigInterface::class); $navigationConfig->setPages( [ [ 'label' => 'Page 1', 'route' => 'page1', 'pages' => [ [ 'label' => 'Page 1.1', 'route' => 'page1/page1-1', 'pages' => [ [ 'label' => 'Page 1.1.1', 'route' => 'page1/page1-1/page1-1-1', ], ], ], ], ], ] ); $container = $serviceLocator->get(Navigation::class); // Removes Page 1.1.1 $container->removePage( $container->findOneBy('route', 'page1/page1-1/page1-1-1'), true );
Finding pages
Containers have two finder methods for retrieving pages. Each recursively searches the container testing for properties with values that match the one provided.
findOneBy($property, $value) : PageInterface|null
: Returns the first page found matching the criteria, ornull
if none was found.findAllBy($property, $value) : array<PageInterface>
: Returns an array of all page instances matching the criteria.
Unlike in Laminas Navigation the
findBy
is not available.
The finder methods can also be used magically by appending the property name to
findBy
, findOneBy
, or findAllBy
. As an example, findOneByLabel('Home')
will return the first matching page with label 'Home'.
Other combinations include findByLabel(...)
, findOneByTitle(...)
,
findAllByController(...)
, etc. Finder methods also work on custom properties,
such as findByFoo('bar')
.
use Mimmi20\Mezzio\Navigation\Navigation; $navigationConfig = $serviceLocator->get(NavigationConfigInterface::class); $navigationConfig->setPages([ [ 'label' => 'Page 1', 'uri' => 'page-1', 'foo' => 'bar', 'pages' => [ [ 'label' => 'Page 1.1', 'uri' => 'page-1.1', 'foo' => 'bar', ], [ 'label' => 'Page 1.2', 'uri' => 'page-1.2', 'class' => 'my-class', ], [ 'type' => 'uri', 'label' => 'Page 1.3', 'uri' => 'page-1.3', 'action' => 'about', ], ], ], [ 'label' => 'Page 2', 'id' => 'page_2_and_3', 'class' => 'my-class', 'module' => 'page2', 'controller' => 'index', 'action' => 'page1', ], [ 'label' => 'Page 3', 'id' => 'page_2_and_3', 'module' => 'page3', 'controller' => 'index', ], ]); $container = $serviceLocator->get(Navigation::class); // The 'id' is not required to be unique, but be aware that // having two pages with the same id will render the same id attribute // in menus and breadcrumbs. // Returns "Page 2": $found = $container->findBy('id', 'page_2_and_3'); // Returns "Page 2": $found = $container->findOneBy('id', 'page_2_and_3'); // Returns "Page 2": $found = $container->findOneById('page_2_and_3'); // Returns "Page 2" AND "Page 3": $found = $container->findAllById('page_2_and_3'); // Find all pages matching the CSS class "my-class": // Returns "Page 1.2" and "Page 2": $found = $container->findAllBy('class', 'my-class'); $found = $container->findAllByClass('my-class'); // Find first page matching CSS class "my-class": // Returns "Page 1.2": $found = $container->findOneByClass('my-class'); // Find all pages matching the CSS class "non-existent": // Returns an empty array. $found = $container->findAllByClass('non-existent'); // Find first page matching the CSS class "non-existent": // Returns null. $found = $container->findOneByClass('non-existent'); // Find all pages with custom property 'foo' = 'bar': // Returns "Page 1" and "Page 1.1": $found = $container->findAllBy('foo', 'bar'); // To achieve the same magically, 'foo' must be in lowercase. // This is because 'foo' is a custom property, and thus the // property name is not normalized to 'Foo': $found = $container->findAllByfoo('bar'); // Find all with controller = 'index': // Returns "Page 2" and "Page 3": $found = $container->findAllByController('index');
Iterating containers
Mezzio\Navigation\ContainerInterface
extends RecursiveIterator
. iterate a
container recursively, use the RecursiveIteratorIterator
class.
use RecursiveIteratorIterator; use Mimmi20\Mezzio\Navigation\Navigation; /* * Create a container from an array */ $navigationConfig = $serviceLocator->get(NavigationConfigInterface::class); $navigationConfig->setPages([ [ 'label' => 'Page 1', 'uri' => '#', ], [ 'label' => 'Page 2', 'uri' => '#', 'pages' => [ [ 'label' => 'Page 2.1', 'uri' => '#', ], [ 'label' => 'Page 2.2', 'uri' => '#', ], ], ], [ 'label' => 'Page 3', 'uri' => '#', ], ]); $container = $serviceLocator->get(Navigation::class); // Iterate flat using regular foreach: // Output: Page 1, Page 2, Page 3 foreach ($container as $page) { echo $page->label; } // Iterate recursively using RecursiveIteratorIterator $it = new RecursiveIteratorIterator( $container, RecursiveIteratorIterator::SELF_FIRST ); // Output: Page 1, Page 2, Page 2.1, Page 2.2, Page 3 foreach ($it as $page) { echo $page->label; }
Other operations
hasPage
hasPage(PageInterface $page) : bool
Check if the container has the given page.
hasPages
hasPages() : bool
Checks if there are any pages in the container, and is equivalent to
count($container) > 0
.
toArray
toArray() : array
Converts the container and the pages in it to a (nested) array. This can be useful for serializing and debugging.
use Mimmi20\Mezzio\Navigation\Navigation; $navigationConfig = $serviceLocator->get(NavigationConfigInterface::class); $navigationConfig->setPages([ [ 'label' => 'Page 1', 'uri' => '#', ], [ 'label' => 'Page 2', 'uri' => '#', 'pages' => [ [ 'label' => 'Page 2.1', 'uri' => '#', ], [ 'label' => 'Page 2.2', 'uri' => '#', ], ], ], ]); $container = $serviceLocator->get(Navigation::class); var_dump($container->toArray()); /* Output: array(2) { [0]=> array(15) { ["label"]=> string(6) "Page 1" ["id"]=> NULL ["class"]=> NULL ["title"]=> NULL ["target"]=> NULL ["rel"]=> array(0) { } ["rev"]=> array(0) { } ["order"]=> NULL ["resource"]=> NULL ["privilege"]=> NULL ["active"]=> bool(false) ["visible"]=> bool(true) ["type"]=> string(23) "Mezzio\Navigation\Page\Uri" ["pages"]=> array(0) { } ["uri"]=> string(1) "#" } [1]=> array(15) { ["label"]=> string(6) "Page 2" ["id"]=> NULL ["class"]=> NULL ["title"]=> NULL ["target"]=> NULL ["rel"]=> array(0) { } ["rev"]=> array(0) { } ["order"]=> NULL ["resource"]=> NULL ["privilege"]=> NULL ["active"]=> bool(false) ["visible"]=> bool(true) ["type"]=> string(23) "Mezzio\Navigation\Page\Uri" ["pages"]=> array(2) { [0]=> array(15) { ["label"]=> string(8) "Page 2.1" ["id"]=> NULL ["class"]=> NULL ["title"]=> NULL ["target"]=> NULL ["rel"]=> array(0) { } ["rev"]=> array(0) { } ["order"]=> NULL ["resource"]=> NULL ["privilege"]=> NULL ["active"]=> bool(false) ["visible"]=> bool(true) ["type"]=> string(23) "Mezzio\Navigation\Page\Uri" ["pages"]=> array(0) { } ["uri"]=> string(1) "#" } [1]=> array(15) { ["label"]=> string(8) "Page 2.2" ["id"]=> NULL ["class"]=> NULL ["title"]=> NULL ["target"]=> NULL ["rel"]=> array(0) { } ["rev"]=> array(0) { } ["order"]=> NULL ["resource"]=> NULL ["privilege"]=> NULL ["active"]=> bool(false) ["visible"]=> bool(true) ["type"]=> string(23) "Mezzio\Navigation\Page\Uri" ["pages"]=> array(0) { } ["uri"]=> string(1) "#" } } ["uri"]=> string(1) "#" } } */
Pages
mezzio-navigation ships with two page types:
- Route pages, using the class
Mezzio\Navigation\Page\Route
- URI pages, using the class
Mezzio\Navigation\Page\Uri
Route pages link to on-site web pages, and are defined using Route parameters
(route
, params
). URI pages are defined by a single
property uri
, which give you the full flexibility to link off-site pages or do
other things with the generated links (e.g. a URI that turns into <a href="#">foo<a>
).
Route Pages replace the MVC Pages from Laminas Navigation.
Unlike in Laminas Navigation the options
controller
andaction
are not supported,
Common page features
All page classes must extend Mezzio\Navigation\Page\PageInterface
, and will thus
share a common set of features and properties. Most notably, they share the
options in the table below and the same initialization process.
Option keys are mapped to set*()
methods. This means that the option order
maps to the method
setOrder()
, and reset_params
maps to the method setResetParams()
. If there is no setter
method for the option, it will be set as a custom property of the page.
Read more on extending Mezzio\Navigation\Page\PageInterface
in the section
"Creating custom page types".
Common page options
Custom properties
All pages support setting and retrieval of custom properties by use of the magic methods
__set($name, $value)
,__get($name)
,__isset($name)
and__unset($name)
. Custom properties may have any value, and will be included in the array that is returned from$page->toArray()
, which means that pages can be serialized/deserialized successfully even if the pages contains properties that are not native in the page class.Both native and custom properties can be set using
$page->set($name, $value)
and retrieved using$page->get($name)
, or by using magic methods.The following example demonstrates custom properties:
$page = new Mimmi20\Mezzio\Navigation\Page\Route(); $page->foo = 'bar'; $page->meaning = 42; echo $page->foo; if ($page->meaning != 42) { // action should be taken }
Route pages
Routes can be used with Route pages. If a page has a route, this route will be
used in getHref()
to generate href
attributes, and the isActive()
method will compare the
Mezzio\Router\RouteResult
params with the page's params to determine if the page
is active.
useRouteMatch flag
If you want to re-use any matched route parameters
when generating a link, you can do so via the useRouteMatch
flag. This is
particularly useful when creating segment routes that include the currently
selected language or locale as an initial segment, as it ensures the links
generated all include the matched value.
Route page options
isActive() determines if page is active
This example demonstrates that Route pages determine whether they are active by using the params found in the route match object.
use Mimmi20\Mezzio\Navigation\Page; /** * Dispatched request: * - route: index */ $page1 = new Page\Route([ 'route' => 'index', ]); $page2 = new Page\Route([ 'route' => 'edit', ]); $page1->isActive(); // returns true $page2->isActive(); // returns false /** * Dispatched request: * - route: edit * - id: 1337 */ $page = new Page\Route([ 'route' => 'edit', 'params' => ['id' => 1337], ]); // returns true, because request has the same route $page->isActive(); /** * Dispatched request: * - route: edit */ $page = new Page\Route([ 'route' => 'edit', 'params' => ['id' => null], ]); // returns false, because page requires the id param to be set in the request $page->isActive(); // returns false
URI Pages
Pages of type Mezzio\Navigation\Page\Uri
can be used to link to pages on other
domains or sites, or to implement custom logic for the page. In addition to the
common page options, a URI page takes only one additional option, a uri
. The
uri
will be returned when calling $page->getHref()
, and may be a string
or
null
.
No auto-determination of active status
Mezzio\Navigation\Page\Uri
will not try to determine whether it should be active when calling$page->isActive()
; it merely returns what currently is set. In order to make a URI page active, you must manually call$page->setActive()
or specify theactive
as a page option during instantiation.
URI page options
Creating custom page types
When implementing Mezzio\Navigation\Page\PageInterface
and using the Mezzio\Navigation\Page\PageTrait
, there is usually no need to
override the constructor or the setOptions()
method. The page constructor
takes a single parameter, an iterable
, which is then
passed to setOptions()
. That method will in turn call the appropriate set*()
methods based on the options provided, which in turn maps the option to native
or custom properties. If the option internal_id
is given, the method will
first look for a method named setInternalId()
, and pass the option to this
method if it exists. If the method does not exist, the option will be set as a
custom property of the page, and be accessible via $internalId = $page->internal_id;
or $internalId = $page->get('internal_id');
.
Basic custom page example
The only thing a custom page class needs to implement is the getHref()
method.
namespace My; use Mimmi20\Mezzio\Navigation\Page\PageInterface; use Mimmi20\Mezzio\Navigation\Page\PageTrait; class Page implements PageInterface { use PageTrait; public function getHref(): string { return 'something-completely-different'; } }
A custom page with properties
When adding properties to an extended page, there is no need to override/modify
setOptions()
.
namespace My\Navigation; use Mimmi20\Mezzio\Navigation\Page\PageInterface; class Page implements PageInterface { protected $foo; protected $fooBar; public function setFoo($foo) { $this->foo = $foo; } public function getFoo() { return $this->foo; } public function setFooBar($fooBar) { $this->fooBar = $fooBar; } public function getFooBar() { return $this->fooBar; } public function getHref(): string { return sprintf('%s/%s', $this->foo, $this->fooBar); } } // Instantiation: $page = new Page([ 'label' => 'Property names are mapped to setters', 'foo' => 'bar', 'foo_bar' => 'baz', ]);
Creating pages using the page factory
All pages (also custom classes), can be created using the page factory,
Mezzio\Navigation\Page\PageFactory
. The factory accepts either an
iterable
set of options. Each key in the options corresponds to a
page option, as seen earlier. If the option uri
is given and no Route options
are provided (route
), a URI page will be
created. If any of the Route options are given, an Route page will be created.
If type
is given, the factory will assume the value to be the name of the
class that should be created. If the value is route
or uri
, an Route or URI page
will be created, respectively.
Creating an Route page using the page factory
use Mimmi20\Mezzio\Navigation\Page\PageInterface; // Route page, as "route" is defined $page = (new PageFactory())->factory([ 'label' => 'Home', 'route' => 'home', ]); // Route page, as "type" is "route" $page = (new PageFactory())->factory([ 'type' => 'route', 'label' => 'My Route page', ]);
Creating a URI page using the page factory
use Mimmi20\Mezzio\Navigation\Page\PageInterface; // URI page, as "uri" is present, with now MVC options $page = (new PageFactory())->factory([ 'label' => 'My URI page', 'uri' => 'http://www.example.com/', ]); // URI page, as "uri" is present $page = (new PageFactory())->factory([ 'label' => 'Search', 'uri' => 'http://www.example.com/search', 'active' => true, ]); // URI page, as "uri" is present $page = (new PageFactory())->factory([ 'label' => 'My URI page', 'uri' => '#', ]); // URI page, as "type" is "uri" $page = (new PageFactory())->factory([ 'type' => 'uri', 'label' => 'My URI page', ]);
Creating a custom page type using the page factory
To create a custom page type using the factory, use the option type
to specify
a class name to instantiate.
namespace My\Navigation; use Mimmi20\Mezzio\Navigation\Page\PageInterface; class Page extends PageInterface { protected $fooBar = 'ok'; public function setFooBar($fooBar) { $this->fooBar = $fooBar; } } // Creates Page instance, as "type" refers to its class. $page = (new PageFactory())->factory([ 'type' => Page::class, 'label' => 'My custom page', 'foo_bar' => 'foo bar', ]);
Quick Start
Usage in a mezzio-based application
The fastest way to get up and running with mezzio-navigation is:
- Register mezzio-navigation as module.
- Define navigation container configuration under the top-level
navigation
key in your application configuration. - Render your container using a navigation view helper within your view scripts.
Register mezzio-navigation as module
Edit the application configuration file config/application.config.php
:
<?php return [ 'modules' => [ 'Mezzio\Router', 'Mezzio\Navigation', // <-- Add this line // ... ], ];
Add the NavigationMiddleware to the pipeline
<?php return [ 'middleware' => [ 'Mezzio\Authentication\AuthenticationMiddleware', // <-- not required 'Mimmi20\Mezzio\Navigation\NavigationMiddleware', // <-- Add this line // ... ], ];
If you need the Navigation inside the Layout, and the Layout is used also for the Not-Found-Page, you have to add the Middleware in the Pipeline before the Routing.
$app->pipe(\Mimmi20\Mezzio\Navigation\NavigationMiddleware::class); // Register the routing middleware in the middleware pipeline. // This middleware registers the Mezzio\Router\RouteResult request attribute. $app->pipe(RouteMiddleware::class);
Navigation container configuration
Add the container definition to your configuration file, e.g.
config/autoload/global.php
:
<?php return [ // ... 'navigation' => [ 'default' => [ [ 'label' => 'Home', 'route' => 'home', ], [ 'label' => 'Page #1', 'route' => 'page-1', 'pages' => [ [ 'label' => 'Child #1', 'route' => 'page-1-child', ], ], ], [ 'label' => 'Page #2', 'route' => 'page-2', ], ], ], // ... ];
Render the navigation
Mezzio support multiple view renders.
One renderer is available:
- mezzio-navigation-laminasviewrenderer, which implements (Mezzios LaminasViewRenderer)
License
This package is licensed using the MIT License.
Please have a look at LICENSE.md
.