├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── AbstractContainer.php ├── ConfigProvider.php ├── Exception ├── BadMethodCallException.php ├── DomainException.php ├── ExceptionInterface.php ├── InvalidArgumentException.php └── OutOfBoundsException.php ├── Module.php ├── Navigation.php ├── Page ├── AbstractPage.php ├── Mvc.php └── Uri.php ├── Service ├── AbstractNavigationFactory.php ├── ConstructedNavigationFactory.php ├── DefaultNavigationFactory.php └── NavigationAbstractServiceFactory.php └── View ├── HelperConfig.php ├── NavigationHelperFactory.php └── ViewHelperManagerDelegatorFactory.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 2.9.2 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 2.9.1 - 2019-08-21 28 | 29 | ### Added 30 | 31 | - [#77](https://github.com/zendframework/zend-navigation/pull/77) adds support for PHP 7.3. 32 | 33 | ### Changed 34 | 35 | - Nothing. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - Nothing. 44 | 45 | ### Fixed 46 | 47 | - Nothing. 48 | 49 | ## 2.9.0 - 2018-04-25 50 | 51 | ### Added 52 | 53 | - [#67](https://github.com/zendframework/zend-navigation/pull/67) adds support for PHP 7.2. 54 | 55 | ### Changed 56 | 57 | - Nothing. 58 | 59 | ### Deprecated 60 | 61 | - Nothing. 62 | 63 | ### Removed 64 | 65 | - [#67](https://github.com/zendframework/zend-navigation/pull/67) removes support for HHVM. 66 | 67 | - [#59](https://github.com/zendframework/zend-navigation/pull/59) removes support for PHP 5.5. 68 | 69 | ### Fixed 70 | 71 | - Nothing. 72 | 73 | ## 2.8.2 - 2017-03-22 74 | 75 | ### Added 76 | 77 | - Nothing. 78 | 79 | ### Deprecated 80 | 81 | - Nothing. 82 | 83 | ### Removed 84 | 85 | - Nothing. 86 | 87 | ### Fixed 88 | 89 | - [#40](https://github.com/zendframework/zend-navigation/pull/40) fixes an 90 | incorrect exception thrown from `Zend\Navigation\Page\Mvc`. 91 | 92 | ## 2.8.1 - 2016-06-12 93 | 94 | ### Added 95 | 96 | - Nothing. 97 | 98 | ### Deprecated 99 | 100 | - Nothing. 101 | 102 | ### Removed 103 | 104 | - Nothing. 105 | 106 | ### Fixed 107 | 108 | - [#38](https://github.com/zendframework/zend-navigation/pull/38) fixes the 109 | `AbstractNavigationFactory` to allow either zend-router or zend-mvc v2 110 | `RouteMatch` or `RouteStackInterface` implementations when injecting pages 111 | with URIs. 112 | 113 | ## 2.8.0 - 2016-06-11 114 | 115 | ### Added 116 | 117 | - [#33](https://github.com/zendframework/zend-navigation/pull/33) adds support 118 | for zend-mvc v3.0. Specifically, the `Mvc` page type now allows usage of 119 | either `Zend\Mvc\Router` or `Zend\Router` for URI generation. 120 | 121 | ### Deprecated 122 | 123 | - Nothing. 124 | 125 | ### Removed 126 | 127 | - Nothing. 128 | 129 | ### Fixed 130 | 131 | - Nothing. 132 | 133 | ## 2.7.2 - 2016-06-11 134 | 135 | ### Added 136 | 137 | - [#27](https://github.com/zendframework/zend-navigation/pull/27) adds and 138 | publishes the documentation to https://zendframework.github.io/zend-navigation/ 139 | 140 | ### Deprecated 141 | 142 | - Nothing. 143 | 144 | ### Removed 145 | 146 | - Nothing. 147 | 148 | ### Fixed 149 | 150 | - [#35](https://github.com/zendframework/zend-navigation/pull/35) fixes errors 151 | in the `ConfigProvider` that prevented its use. 152 | 153 | ## 2.7.1 - 2016-04-08 154 | 155 | ### Added 156 | 157 | - Nothing. 158 | 159 | ### Deprecated 160 | 161 | - Nothing. 162 | 163 | ### Removed 164 | 165 | - Nothing. 166 | 167 | ### Fixed 168 | 169 | - This release removes the erroneous calls to `getViewHelperConfig()` in the 170 | `ConfigProvider` and `Module` classes. 171 | 172 | ## 2.7.0 - 2016-04-08 173 | 174 | ### Added 175 | 176 | - [#26](https://github.com/zendframework/zend-navigation/pull/26) adds: 177 | - `Zend\Navigation\View\ViewHelperManagerDelegatorFactory`, which decorates 178 | the `ViewHelperManager` service to configure it using 179 | `Zend\Navigation\View\HelperConfig`. 180 | - `ConfigProvider`, which maps the default navigation factory and the 181 | navigation abstract factory, as well as the navigation view helper. 182 | - `Module`, which does the same as the above, but for zend-mvc 183 | applications. 184 | 185 | ### Deprecated 186 | 187 | - Nothing. 188 | 189 | ### Removed 190 | 191 | - Nothing. 192 | 193 | ### Fixed 194 | 195 | - Nothing. 196 | 197 | ## 2.6.1 - 2016-03-21 198 | 199 | ### Added 200 | 201 | - Nothing. 202 | 203 | ### Deprecated 204 | 205 | - Nothing. 206 | 207 | ### Removed 208 | 209 | - Nothing. 210 | 211 | ### Fixed 212 | 213 | - [#25](https://github.com/zendframework/zend-navigation/pull/25) ups the 214 | minimum zend-view version to 2.6.5, to bring in a fix for a circular 215 | dependency issue in the navigation helpers. 216 | 217 | ## 2.6.0 - 2016-02-24 218 | 219 | ### Added 220 | 221 | - Nothing. 222 | 223 | ### Deprecated 224 | 225 | - Nothing. 226 | 227 | ### Removed 228 | 229 | - Nothing. 230 | 231 | ### Fixed 232 | 233 | - [#5](https://github.com/zendframework/zend-navigation/pull/5) and 234 | [#20](https://github.com/zendframework/zend-navigation/pull/20) update the 235 | code to be forwards compatible with zend-servicemanager v3. 236 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2019, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zend-navigation 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [laminas/laminas-navigation](https://github.com/laminas/laminas-navigation). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-navigation.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-navigation) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-navigation/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-navigation?branch=master) 9 | 10 | `Zend\Navigation` is a component for managing trees of pointers to web pages. 11 | Simply put: It can be used for creating menus, breadcrumbs, links, and sitemaps, 12 | or serve as a model for other navigation related purposes. 13 | 14 | 15 | - File issues at https://github.com/zendframework/zend-navigation/issues 16 | - Documentation is at https://docs.zendframework.com/zend-navigation/ 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-navigation", 3 | "description": "Manage trees of pointers to web pages in order to build navigation systems", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "zendframework", 7 | "zf", 8 | "navigation" 9 | ], 10 | "support": { 11 | "docs": "https://docs.zendframework.com/zend-navigation/", 12 | "issues": "https://github.com/zendframework/zend-navigation/issues", 13 | "source": "https://github.com/zendframework/zend-navigation", 14 | "rss": "https://github.com/zendframework/zend-navigation/releases.atom", 15 | "chat": "https://zendframework-slack.herokuapp.com", 16 | "forum": "https://discourse.zendframework.com/c/questions/components" 17 | }, 18 | "require": { 19 | "php": "^5.6 || ^7.0", 20 | "zendframework/zend-stdlib": "^2.7 || ^3.0" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", 24 | "zendframework/zend-coding-standard": "~1.0.0", 25 | "zendframework/zend-config": "^2.6 || ^3.1", 26 | "zendframework/zend-console": "^2.6", 27 | "zendframework/zend-http": "^2.6", 28 | "zendframework/zend-i18n": "^2.7.3", 29 | "zendframework/zend-log": "^2.9.1", 30 | "zendframework/zend-mvc": "^2.7.9 || ^3.0.4", 31 | "zendframework/zend-permissions-acl": "^2.6", 32 | "zendframework/zend-router": "^3.0.2", 33 | "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", 34 | "zendframework/zend-uri": "^2.5.2", 35 | "zendframework/zend-view": "^2.9" 36 | }, 37 | "suggest": { 38 | "zendframework/zend-config": "^2.6 || ^3.1, to provide page configuration (optional, as arrays and Traversables are also allowed)", 39 | "zendframework/zend-permissions-acl": "^2.6, to provide ACL-based access restrictions to pages", 40 | "zendframework/zend-router": "^3.0, to use router-based URI generation with Mvc pages", 41 | "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3, to use the navigation factories", 42 | "zendframework/zend-view": "^2.8.1, to use the navigation view helpers" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Zend\\Navigation\\": "src/" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "ZendTest\\Navigation\\": "test/" 52 | } 53 | }, 54 | "config": { 55 | "sort-packages": true 56 | }, 57 | "extra": { 58 | "branch-alias": { 59 | "dev-master": "2.9.x-dev", 60 | "dev-develop": "2.10.x-dev" 61 | }, 62 | "zf": { 63 | "component": "Zend\\Navigation", 64 | "config-provider": "Zend\\Navigation\\ConfigProvider" 65 | } 66 | }, 67 | "scripts": { 68 | "check": [ 69 | "@cs-check", 70 | "@test" 71 | ], 72 | "cs-check": "phpcs", 73 | "cs-fix": "phpcbf", 74 | "test": "phpunit --colors=always", 75 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/AbstractContainer.php: -------------------------------------------------------------------------------- 1 | dirtyIndex) { 56 | return; 57 | } 58 | 59 | $newIndex = []; 60 | $index = 0; 61 | 62 | foreach ($this->pages as $hash => $page) { 63 | $order = $page->getOrder(); 64 | if ($order === null) { 65 | $newIndex[$hash] = $index; 66 | $index++; 67 | } else { 68 | $newIndex[$hash] = $order; 69 | } 70 | } 71 | 72 | asort($newIndex); 73 | $this->index = $newIndex; 74 | $this->dirtyIndex = false; 75 | } 76 | 77 | // Public methods: 78 | 79 | /** 80 | * Notifies container that the order of pages are updated 81 | * 82 | * @return void 83 | */ 84 | public function notifyOrderUpdated() 85 | { 86 | $this->dirtyIndex = true; 87 | } 88 | 89 | /** 90 | * Adds a page to the container 91 | * 92 | * This method will inject the container as the given page's parent by 93 | * calling {@link Page\AbstractPage::setParent()}. 94 | * 95 | * @param Page\AbstractPage|array|Traversable $page page to add 96 | * @return self fluent interface, returns self 97 | * @throws Exception\InvalidArgumentException if page is invalid 98 | */ 99 | public function addPage($page) 100 | { 101 | if ($page === $this) { 102 | throw new Exception\InvalidArgumentException( 103 | 'A page cannot have itself as a parent' 104 | ); 105 | } 106 | 107 | if (! $page instanceof Page\AbstractPage) { 108 | if (! is_array($page) && ! $page instanceof Traversable) { 109 | throw new Exception\InvalidArgumentException( 110 | 'Invalid argument: $page must be an instance of ' 111 | . 'Zend\Navigation\Page\AbstractPage or Traversable, or an array' 112 | ); 113 | } 114 | $page = Page\AbstractPage::factory($page); 115 | } 116 | 117 | $hash = $page->hashCode(); 118 | 119 | if (array_key_exists($hash, $this->index)) { 120 | // page is already in container 121 | return $this; 122 | } 123 | 124 | // adds page to container and sets dirty flag 125 | $this->pages[$hash] = $page; 126 | $this->index[$hash] = $page->getOrder(); 127 | $this->dirtyIndex = true; 128 | 129 | // inject self as page parent 130 | $page->setParent($this); 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * Adds several pages at once 137 | * 138 | * @param array|Traversable|AbstractContainer $pages pages to add 139 | * @return self fluent interface, returns self 140 | * @throws Exception\InvalidArgumentException if $pages is not array, 141 | * Traversable or AbstractContainer 142 | */ 143 | public function addPages($pages) 144 | { 145 | if (! is_array($pages) && ! $pages instanceof Traversable) { 146 | throw new Exception\InvalidArgumentException( 147 | 'Invalid argument: $pages must be an array, an ' 148 | . 'instance of Traversable or an instance of ' 149 | . 'Zend\Navigation\AbstractContainer' 150 | ); 151 | } 152 | 153 | // Because adding a page to a container removes it from the original 154 | // (see {@link Page\AbstractPage::setParent()}), iteration of the 155 | // original container will break. As such, we need to iterate the 156 | // container into an array first. 157 | if ($pages instanceof AbstractContainer) { 158 | $pages = iterator_to_array($pages); 159 | } 160 | 161 | foreach ($pages as $page) { 162 | if (null === $page) { 163 | continue; 164 | } 165 | $this->addPage($page); 166 | } 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * Sets pages this container should have, removing existing pages 173 | * 174 | * @param array $pages pages to set 175 | * @return self fluent interface, returns self 176 | */ 177 | public function setPages(array $pages) 178 | { 179 | $this->removePages(); 180 | return $this->addPages($pages); 181 | } 182 | 183 | /** 184 | * Returns pages in the container 185 | * 186 | * @return array array of Page\AbstractPage instances 187 | */ 188 | public function getPages() 189 | { 190 | return $this->pages; 191 | } 192 | 193 | /** 194 | * Removes the given page from the container 195 | * 196 | * @param Page\AbstractPage|int $page page to remove, either a page 197 | * instance or a specific page order 198 | * @param bool $recursive [optional] whether to remove recursively 199 | * @return bool whether the removal was successful 200 | */ 201 | public function removePage($page, $recursive = false) 202 | { 203 | if ($page instanceof Page\AbstractPage) { 204 | $hash = $page->hashCode(); 205 | } elseif (is_int($page)) { 206 | $this->sort(); 207 | if (! $hash = array_search($page, $this->index)) { 208 | return false; 209 | } 210 | } else { 211 | return false; 212 | } 213 | 214 | if (isset($this->pages[$hash])) { 215 | unset($this->pages[$hash]); 216 | unset($this->index[$hash]); 217 | $this->dirtyIndex = true; 218 | return true; 219 | } 220 | 221 | if ($recursive) { 222 | /** @var \Zend\Navigation\Page\AbstractPage $childPage */ 223 | foreach ($this->pages as $childPage) { 224 | if ($childPage->hasPage($page, true)) { 225 | $childPage->removePage($page, true); 226 | return true; 227 | } 228 | } 229 | } 230 | 231 | return false; 232 | } 233 | 234 | /** 235 | * Removes all pages in container 236 | * 237 | * @return self fluent interface, returns self 238 | */ 239 | public function removePages() 240 | { 241 | $this->pages = []; 242 | $this->index = []; 243 | return $this; 244 | } 245 | 246 | /** 247 | * Checks if the container has the given page 248 | * 249 | * @param Page\AbstractPage $page page to look for 250 | * @param bool $recursive [optional] whether to search recursively. 251 | * Default is false. 252 | * @return bool whether page is in container 253 | */ 254 | public function hasPage(Page\AbstractPage $page, $recursive = false) 255 | { 256 | if (array_key_exists($page->hashCode(), $this->index)) { 257 | return true; 258 | } elseif ($recursive) { 259 | foreach ($this->pages as $childPage) { 260 | if ($childPage->hasPage($page, true)) { 261 | return true; 262 | } 263 | } 264 | } 265 | 266 | return false; 267 | } 268 | 269 | /** 270 | * Returns true if container contains any pages 271 | * 272 | * @param bool $onlyVisible whether to check only visible pages 273 | * @return bool whether container has any pages 274 | */ 275 | public function hasPages($onlyVisible = false) 276 | { 277 | if ($onlyVisible) { 278 | foreach ($this->pages as $page) { 279 | if ($page->isVisible()) { 280 | return true; 281 | } 282 | } 283 | // no visible pages found 284 | return false; 285 | } 286 | return $this->index ? true : false; 287 | } 288 | 289 | /** 290 | * Returns a child page matching $property == $value, or null if not found 291 | * 292 | * @param string $property name of property to match against 293 | * @param mixed $value value to match property against 294 | * @return Page\AbstractPage|null matching page or null 295 | */ 296 | public function findOneBy($property, $value) 297 | { 298 | $iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST); 299 | 300 | foreach ($iterator as $page) { 301 | if ($page->get($property) == $value) { 302 | return $page; 303 | } 304 | } 305 | 306 | return; 307 | } 308 | 309 | /** 310 | * Returns all child pages matching $property == $value, or an empty array 311 | * if no pages are found 312 | * 313 | * @param string $property name of property to match against 314 | * @param mixed $value value to match property against 315 | * @return array array containing only Page\AbstractPage instances 316 | */ 317 | public function findAllBy($property, $value) 318 | { 319 | $found = []; 320 | 321 | $iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST); 322 | 323 | foreach ($iterator as $page) { 324 | if ($page->get($property) == $value) { 325 | $found[] = $page; 326 | } 327 | } 328 | 329 | return $found; 330 | } 331 | 332 | /** 333 | * Returns page(s) matching $property == $value 334 | * 335 | * @param string $property name of property to match against 336 | * @param mixed $value value to match property against 337 | * @param bool $all [optional] whether an array of all matching 338 | * pages should be returned, or only the first. 339 | * If true, an array will be returned, even if not 340 | * matching pages are found. If false, null will 341 | * be returned if no matching page is found. 342 | * Default is false. 343 | * @return Page\AbstractPage|null matching page or null 344 | */ 345 | public function findBy($property, $value, $all = false) 346 | { 347 | if ($all) { 348 | return $this->findAllBy($property, $value); 349 | } 350 | 351 | return $this->findOneBy($property, $value); 352 | } 353 | 354 | /** 355 | * Magic overload: Proxy calls to finder methods 356 | * 357 | * Examples of finder calls: 358 | * 359 | * // METHOD // SAME AS 360 | * $nav->findByLabel('foo'); // $nav->findOneBy('label', 'foo'); 361 | * $nav->findOneByLabel('foo'); // $nav->findOneBy('label', 'foo'); 362 | * $nav->findAllByClass('foo'); // $nav->findAllBy('class', 'foo'); 363 | * 364 | * 365 | * @param string $method method name 366 | * @param array $arguments method arguments 367 | * @throws Exception\BadMethodCallException if method does not exist 368 | */ 369 | public function __call($method, $arguments) 370 | { 371 | ErrorHandler::start(E_WARNING); 372 | $result = preg_match('/(find(?:One|All)?By)(.+)/', $method, $match); 373 | $error = ErrorHandler::stop(); 374 | if (! $result) { 375 | throw new Exception\BadMethodCallException(sprintf( 376 | 'Bad method call: Unknown method %s::%s', 377 | get_class($this), 378 | $method 379 | ), 0, $error); 380 | } 381 | return $this->{$match[1]}($match[2], $arguments[0]); 382 | } 383 | 384 | /** 385 | * Returns an array representation of all pages in container 386 | * 387 | * @return array 388 | */ 389 | public function toArray() 390 | { 391 | $this->sort(); 392 | $pages = []; 393 | $indexes = array_keys($this->index); 394 | foreach ($indexes as $hash) { 395 | $pages[] = $this->pages[$hash]->toArray(); 396 | } 397 | return $pages; 398 | } 399 | 400 | // RecursiveIterator interface: 401 | 402 | /** 403 | * Returns current page 404 | * 405 | * Implements RecursiveIterator interface. 406 | * 407 | * @return Page\AbstractPage current page or null 408 | * @throws Exception\OutOfBoundsException if the index is invalid 409 | */ 410 | public function current() 411 | { 412 | $this->sort(); 413 | 414 | current($this->index); 415 | $hash = key($this->index); 416 | if (! isset($this->pages[$hash])) { 417 | throw new Exception\OutOfBoundsException( 418 | 'Corruption detected in container; ' 419 | . 'invalid key found in internal iterator' 420 | ); 421 | } 422 | 423 | return $this->pages[$hash]; 424 | } 425 | 426 | /** 427 | * Returns hash code of current page 428 | * 429 | * Implements RecursiveIterator interface. 430 | * 431 | * @return string hash code of current page 432 | */ 433 | public function key() 434 | { 435 | $this->sort(); 436 | return key($this->index); 437 | } 438 | 439 | /** 440 | * Moves index pointer to next page in the container 441 | * 442 | * Implements RecursiveIterator interface. 443 | * 444 | * @return void 445 | */ 446 | public function next() 447 | { 448 | $this->sort(); 449 | next($this->index); 450 | } 451 | 452 | /** 453 | * Sets index pointer to first page in the container 454 | * 455 | * Implements RecursiveIterator interface. 456 | * 457 | * @return void 458 | */ 459 | public function rewind() 460 | { 461 | $this->sort(); 462 | reset($this->index); 463 | } 464 | 465 | /** 466 | * Checks if container index is valid 467 | * 468 | * Implements RecursiveIterator interface. 469 | * 470 | * @return bool 471 | */ 472 | public function valid() 473 | { 474 | $this->sort(); 475 | return current($this->index) !== false; 476 | } 477 | 478 | /** 479 | * Proxy to hasPages() 480 | * 481 | * Implements RecursiveIterator interface. 482 | * 483 | * @return bool whether container has any pages 484 | */ 485 | public function hasChildren() 486 | { 487 | return $this->valid() && $this->current()->hasPages(); 488 | } 489 | 490 | /** 491 | * Returns the child container. 492 | * 493 | * Implements RecursiveIterator interface. 494 | * 495 | * @return Page\AbstractPage|null 496 | */ 497 | public function getChildren() 498 | { 499 | $hash = key($this->index); 500 | 501 | if (isset($this->pages[$hash])) { 502 | return $this->pages[$hash]; 503 | } 504 | 505 | return; 506 | } 507 | 508 | // Countable interface: 509 | 510 | /** 511 | * Returns number of pages in container 512 | * 513 | * Implements Countable interface. 514 | * 515 | * @return int number of pages in the container 516 | */ 517 | public function count() 518 | { 519 | return count($this->index); 520 | } 521 | } 522 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | $this->getDependencyConfig(), 21 | ]; 22 | } 23 | 24 | /** 25 | * Return application-level dependency configuration. 26 | * 27 | * @return array 28 | */ 29 | public function getDependencyConfig() 30 | { 31 | return [ 32 | 'abstract_factories' => [ 33 | Service\NavigationAbstractServiceFactory::class, 34 | ], 35 | 'aliases' => [ 36 | 'navigation' => Navigation::class, 37 | ], 38 | 'delegators' => [ 39 | 'ViewHelperManager' => [ 40 | View\ViewHelperManagerDelegatorFactory::class, 41 | ], 42 | ], 43 | 'factories' => [ 44 | Navigation::class => Service\DefaultNavigationFactory::class, 45 | ], 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | $provider->getDependencyConfig(), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Navigation.php: -------------------------------------------------------------------------------- 1 | addPages($pages); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Page/AbstractPage.php: -------------------------------------------------------------------------------- 1 | setOptions($options); 284 | } 285 | 286 | // do custom initialization 287 | $this->init(); 288 | } 289 | 290 | /** 291 | * Initializes page (used by subclasses) 292 | * 293 | * @return void 294 | */ 295 | protected function init() 296 | { 297 | } 298 | 299 | /** 300 | * Sets page properties using options from an associative array 301 | * 302 | * Each key in the array corresponds to the according set*() method, and 303 | * each word is separated by underscores, e.g. the option 'target' 304 | * corresponds to setTarget(), and the option 'reset_params' corresponds to 305 | * the method setResetParams(). 306 | * 307 | * @param array $options associative array of options to set 308 | * @return AbstractPage fluent interface, returns self 309 | * @throws Exception\InvalidArgumentException if invalid options are given 310 | */ 311 | public function setOptions(array $options) 312 | { 313 | foreach ($options as $key => $value) { 314 | $this->set($key, $value); 315 | } 316 | 317 | return $this; 318 | } 319 | 320 | // Accessors: 321 | 322 | /** 323 | * Sets page label 324 | * 325 | * @param string $label new page label 326 | * @return AbstractPage fluent interface, returns self 327 | * @throws Exception\InvalidArgumentException if empty/no string is given 328 | */ 329 | public function setLabel($label) 330 | { 331 | if (null !== $label && ! is_string($label)) { 332 | throw new Exception\InvalidArgumentException( 333 | 'Invalid argument: $label must be a string or null' 334 | ); 335 | } 336 | 337 | $this->label = $label; 338 | return $this; 339 | } 340 | 341 | /** 342 | * Returns page label 343 | * 344 | * @return string page label or null 345 | */ 346 | public function getLabel() 347 | { 348 | return $this->label; 349 | } 350 | 351 | /** 352 | * Sets a fragment identifier 353 | * 354 | * @param string $fragment new fragment identifier 355 | * @return AbstractPage fluent interface, returns self 356 | * @throws Exception\InvalidArgumentException if empty/no string is given 357 | */ 358 | public function setFragment($fragment) 359 | { 360 | if (null !== $fragment && ! is_string($fragment)) { 361 | throw new Exception\InvalidArgumentException( 362 | 'Invalid argument: $fragment must be a string or null' 363 | ); 364 | } 365 | 366 | $this->fragment = $fragment; 367 | return $this; 368 | } 369 | 370 | /** 371 | * Returns fragment identifier 372 | * 373 | * @return string|null fragment identifier 374 | */ 375 | public function getFragment() 376 | { 377 | return $this->fragment; 378 | } 379 | 380 | /** 381 | * Sets page id 382 | * 383 | * @param string|null $id [optional] id to set. Default is null, 384 | * which sets no id. 385 | * @return AbstractPage fluent interface, returns self 386 | * @throws Exception\InvalidArgumentException if not given string or null 387 | */ 388 | public function setId($id = null) 389 | { 390 | if (null !== $id && ! is_string($id) && ! is_numeric($id)) { 391 | throw new Exception\InvalidArgumentException( 392 | 'Invalid argument: $id must be a string, number or null' 393 | ); 394 | } 395 | 396 | $this->id = null === $id ? $id : (string) $id; 397 | 398 | return $this; 399 | } 400 | 401 | /** 402 | * Returns page id 403 | * 404 | * @return string|null page id or null 405 | */ 406 | public function getId() 407 | { 408 | return $this->id; 409 | } 410 | 411 | /** 412 | * Sets page CSS class 413 | * 414 | * @param string|null $class [optional] CSS class to set. Default 415 | * is null, which sets no CSS class. 416 | * @return AbstractPage fluent interface, returns self 417 | * @throws Exception\InvalidArgumentException if not given string or null 418 | */ 419 | public function setClass($class = null) 420 | { 421 | if (null !== $class && ! is_string($class)) { 422 | throw new Exception\InvalidArgumentException( 423 | 'Invalid argument: $class must be a string or null' 424 | ); 425 | } 426 | 427 | $this->class = $class; 428 | return $this; 429 | } 430 | 431 | /** 432 | * Returns page class (CSS) 433 | * 434 | * @return string|null page's CSS class or null 435 | */ 436 | public function getClass() 437 | { 438 | return $this->class; 439 | } 440 | 441 | /** 442 | * Sets page title 443 | * 444 | * @param string $title [optional] page title. Default is 445 | * null, which sets no title. 446 | * @return AbstractPage fluent interface, returns self 447 | * @throws Exception\InvalidArgumentException if not given string or null 448 | */ 449 | public function setTitle($title = null) 450 | { 451 | if (null !== $title && ! is_string($title)) { 452 | throw new Exception\InvalidArgumentException( 453 | 'Invalid argument: $title must be a non-empty string' 454 | ); 455 | } 456 | 457 | $this->title = $title; 458 | return $this; 459 | } 460 | 461 | /** 462 | * Returns page title 463 | * 464 | * @return string|null page title or null 465 | */ 466 | public function getTitle() 467 | { 468 | return $this->title; 469 | } 470 | 471 | /** 472 | * Sets page target 473 | * 474 | * @param string|null $target [optional] target to set. Default is 475 | * null, which sets no target. 476 | * 477 | * @return AbstractPage fluent interface, returns self 478 | * @throws Exception\InvalidArgumentException if target is not string or null 479 | */ 480 | public function setTarget($target = null) 481 | { 482 | if (null !== $target && ! is_string($target)) { 483 | throw new Exception\InvalidArgumentException( 484 | 'Invalid argument: $target must be a string or null' 485 | ); 486 | } 487 | 488 | $this->target = $target; 489 | return $this; 490 | } 491 | 492 | /** 493 | * Returns page target 494 | * 495 | * @return string|null page target or null 496 | */ 497 | public function getTarget() 498 | { 499 | return $this->target; 500 | } 501 | 502 | /** 503 | * Sets the page's forward links to other pages 504 | * 505 | * This method expects an associative array of forward links to other pages, 506 | * where each element's key is the name of the relation (e.g. alternate, 507 | * prev, next, help, etc), and the value is a mixed value that could somehow 508 | * be considered a page. 509 | * 510 | * @param array|Traversable $relations [optional] an associative array of 511 | * forward links to other pages 512 | * @throws Exception\InvalidArgumentException if $relations is not an array 513 | * or Traversable object 514 | * @return AbstractPage fluent interface, returns self 515 | */ 516 | public function setRel($relations = null) 517 | { 518 | $this->rel = []; 519 | 520 | if (null !== $relations) { 521 | if ($relations instanceof Traversable) { 522 | $relations = ArrayUtils::iteratorToArray($relations); 523 | } 524 | 525 | if (! is_array($relations)) { 526 | throw new Exception\InvalidArgumentException( 527 | 'Invalid argument: $relations must be an ' . 528 | 'array or an instance of Traversable' 529 | ); 530 | } 531 | 532 | foreach ($relations as $name => $relation) { 533 | if (is_string($name)) { 534 | $this->rel[$name] = $relation; 535 | } 536 | } 537 | } 538 | 539 | return $this; 540 | } 541 | 542 | /** 543 | * Returns the page's forward links to other pages 544 | * 545 | * This method returns an associative array of forward links to other pages, 546 | * where each element's key is the name of the relation (e.g. alternate, 547 | * prev, next, help, etc), and the value is a mixed value that could somehow 548 | * be considered a page. 549 | * 550 | * @param string $relation [optional] name of relation to return. If not 551 | * given, all relations will be returned. 552 | * @return array an array of relations. If $relation is not 553 | * specified, all relations will be returned in 554 | * an associative array. 555 | */ 556 | public function getRel($relation = null) 557 | { 558 | if (null !== $relation) { 559 | return isset($this->rel[$relation]) 560 | ? $this->rel[$relation] 561 | : null; 562 | } 563 | 564 | return $this->rel; 565 | } 566 | 567 | /** 568 | * Sets the page's reverse links to other pages 569 | * 570 | * This method expects an associative array of reverse links to other pages, 571 | * where each element's key is the name of the relation (e.g. alternate, 572 | * prev, next, help, etc), and the value is a mixed value that could somehow 573 | * be considered a page. 574 | * 575 | * @param array|Traversable $relations [optional] an associative array of 576 | * reverse links to other pages 577 | * 578 | * @throws Exception\InvalidArgumentException if $relations it not an array 579 | * or Traversable object 580 | * @return AbstractPage fluent interface, returns self 581 | */ 582 | public function setRev($relations = null) 583 | { 584 | $this->rev = []; 585 | 586 | if (null !== $relations) { 587 | if ($relations instanceof Traversable) { 588 | $relations = ArrayUtils::iteratorToArray($relations); 589 | } 590 | 591 | if (! is_array($relations)) { 592 | throw new Exception\InvalidArgumentException( 593 | 'Invalid argument: $relations must be an ' . 594 | 'array or an instance of Traversable' 595 | ); 596 | } 597 | 598 | foreach ($relations as $name => $relation) { 599 | if (is_string($name)) { 600 | $this->rev[$name] = $relation; 601 | } 602 | } 603 | } 604 | 605 | return $this; 606 | } 607 | 608 | /** 609 | * Returns the page's reverse links to other pages 610 | * 611 | * This method returns an associative array of forward links to other pages, 612 | * where each element's key is the name of the relation (e.g. alternate, 613 | * prev, next, help, etc), and the value is a mixed value that could somehow 614 | * be considered a page. 615 | * 616 | * @param string $relation [optional] name of relation to return. If not 617 | * given, all relations will be returned. 618 | * 619 | * @return array an array of relations. If $relation is not 620 | * specified, all relations will be returned in 621 | * an associative array. 622 | */ 623 | public function getRev($relation = null) 624 | { 625 | if (null !== $relation) { 626 | return isset($this->rev[$relation]) 627 | ? 628 | $this->rev[$relation] 629 | : 630 | null; 631 | } 632 | 633 | return $this->rev; 634 | } 635 | 636 | /** 637 | * Sets page order to use in parent container 638 | * 639 | * @param int $order [optional] page order in container. 640 | * Default is null, which sets no 641 | * specific order. 642 | * @return AbstractPage fluent interface, returns self 643 | * @throws Exception\InvalidArgumentException if order is not integer or null 644 | */ 645 | public function setOrder($order = null) 646 | { 647 | if (is_string($order)) { 648 | $temp = (int) $order; 649 | if ($temp < 0 || $temp > 0 || $order == '0') { 650 | $order = $temp; 651 | } 652 | } 653 | 654 | if (null !== $order && ! is_int($order)) { 655 | throw new Exception\InvalidArgumentException( 656 | 'Invalid argument: $order must be an integer or null, ' . 657 | 'or a string that casts to an integer' 658 | ); 659 | } 660 | 661 | $this->order = $order; 662 | 663 | // notify parent, if any 664 | if (isset($this->parent)) { 665 | $this->parent->notifyOrderUpdated(); 666 | } 667 | 668 | return $this; 669 | } 670 | 671 | /** 672 | * Returns page order used in parent container 673 | * 674 | * @return int|null page order or null 675 | */ 676 | public function getOrder() 677 | { 678 | return $this->order; 679 | } 680 | 681 | /** 682 | * Sets ACL resource associated with this page 683 | * 684 | * @param string|AclResource $resource [optional] resource to associate 685 | * with page. Default is null, which 686 | * sets no resource. 687 | * @return AbstractPage fluent interface, returns self 688 | * @throws Exception\InvalidArgumentException if $resource is invalid 689 | */ 690 | public function setResource($resource = null) 691 | { 692 | if (null === $resource 693 | || is_string($resource) 694 | || $resource instanceof AclResource 695 | ) { 696 | $this->resource = $resource; 697 | } else { 698 | throw new Exception\InvalidArgumentException( 699 | 'Invalid argument: $resource must be null, a string, ' . 700 | 'or an instance of Zend\Permissions\Acl\Resource\ResourceInterface' 701 | ); 702 | } 703 | 704 | return $this; 705 | } 706 | 707 | /** 708 | * Returns ACL resource associated with this page 709 | * 710 | * @return string|AclResource|null ACL resource or null 711 | */ 712 | public function getResource() 713 | { 714 | return $this->resource; 715 | } 716 | 717 | /** 718 | * Sets ACL privilege associated with this page 719 | * 720 | * @param string|null $privilege [optional] ACL privilege to associate 721 | * with this page. Default is null, which 722 | * sets no privilege. 723 | * 724 | * @return AbstractPage fluent interface, returns self 725 | */ 726 | public function setPrivilege($privilege = null) 727 | { 728 | $this->privilege = is_string($privilege) ? $privilege : null; 729 | return $this; 730 | } 731 | 732 | /** 733 | * Returns ACL privilege associated with this page 734 | * 735 | * @return string|null ACL privilege or null 736 | */ 737 | public function getPrivilege() 738 | { 739 | return $this->privilege; 740 | } 741 | 742 | /** 743 | * Sets permission associated with this page 744 | * 745 | * @param mixed|null $permission [optional] permission to associate 746 | * with this page. Default is null, which 747 | * sets no permission. 748 | * 749 | * @return AbstractPage fluent interface, returns self 750 | */ 751 | public function setPermission($permission = null) 752 | { 753 | $this->permission = $permission; 754 | return $this; 755 | } 756 | 757 | /** 758 | * Returns permission associated with this page 759 | * 760 | * @return mixed|null permission or null 761 | */ 762 | public function getPermission() 763 | { 764 | return $this->permission; 765 | } 766 | 767 | /** 768 | * Sets text domain for translation 769 | * 770 | * @param string|null $textDomain [optional] text domain to associate 771 | * with this page. Default is null, which 772 | * sets no text domain. 773 | * 774 | * @return AbstractPage fluent interface, returns self 775 | */ 776 | public function setTextDomain($textDomain = null) 777 | { 778 | if (null !== $textDomain) { 779 | $this->textDomain = $textDomain; 780 | } 781 | return $this; 782 | } 783 | 784 | /** 785 | * Returns text domain for translation 786 | * 787 | * @return mixed|null text domain or null 788 | */ 789 | public function getTextDomain() 790 | { 791 | return $this->textDomain; 792 | } 793 | 794 | /** 795 | * Sets whether page should be considered active or not 796 | * 797 | * @param bool $active [optional] whether page should be 798 | * considered active or not. Default is true. 799 | * 800 | * @return AbstractPage fluent interface, returns self 801 | */ 802 | public function setActive($active = true) 803 | { 804 | $this->active = (bool) $active; 805 | return $this; 806 | } 807 | 808 | /** 809 | * Returns whether page should be considered active or not 810 | * 811 | * @param bool $recursive [optional] whether page should be considered 812 | * active if any child pages are active. Default is 813 | * false. 814 | * @return bool whether page should be considered active 815 | */ 816 | public function isActive($recursive = false) 817 | { 818 | if (! $this->active && $recursive) { 819 | foreach ($this->pages as $page) { 820 | if ($page->isActive(true)) { 821 | return true; 822 | } 823 | } 824 | return false; 825 | } 826 | 827 | return $this->active; 828 | } 829 | 830 | /** 831 | * Proxy to isActive() 832 | * 833 | * @param bool $recursive [optional] whether page should be considered 834 | * active if any child pages are active. Default 835 | * is false. 836 | * 837 | * @return bool whether page should be considered active 838 | */ 839 | public function getActive($recursive = false) 840 | { 841 | return $this->isActive($recursive); 842 | } 843 | 844 | /** 845 | * Sets whether the page should be visible or not 846 | * 847 | * @param bool $visible [optional] whether page should be 848 | * considered visible or not. Default is true. 849 | * @return AbstractPage fluent interface, returns self 850 | */ 851 | public function setVisible($visible = true) 852 | { 853 | if (is_string($visible) && 'false' == strtolower($visible)) { 854 | $visible = false; 855 | } 856 | $this->visible = (bool) $visible; 857 | return $this; 858 | } 859 | 860 | /** 861 | * Returns a boolean value indicating whether the page is visible 862 | * 863 | * @param bool $recursive [optional] whether page should be considered 864 | * invisible if parent is invisible. Default is 865 | * false. 866 | * 867 | * @return bool whether page should be considered visible 868 | */ 869 | public function isVisible($recursive = false) 870 | { 871 | if ($recursive 872 | && isset($this->parent) 873 | && $this->parent instanceof self 874 | ) { 875 | if (! $this->parent->isVisible(true)) { 876 | return false; 877 | } 878 | } 879 | 880 | return $this->visible; 881 | } 882 | 883 | /** 884 | * Proxy to isVisible() 885 | * 886 | * Returns a boolean value indicating whether the page is visible 887 | * 888 | * @param bool $recursive [optional] whether page should be considered 889 | * invisible if parent is invisible. Default is 890 | * false. 891 | * 892 | * @return bool whether page should be considered visible 893 | */ 894 | public function getVisible($recursive = false) 895 | { 896 | return $this->isVisible($recursive); 897 | } 898 | 899 | /** 900 | * Sets parent container 901 | * 902 | * @param AbstractContainer $parent [optional] new parent to set. 903 | * Default is null which will set no parent. 904 | * @throws Exception\InvalidArgumentException 905 | * @return AbstractPage fluent interface, returns self 906 | */ 907 | public function setParent(AbstractContainer $parent = null) 908 | { 909 | if ($parent === $this) { 910 | throw new Exception\InvalidArgumentException( 911 | 'A page cannot have itself as a parent' 912 | ); 913 | } 914 | 915 | // return if the given parent already is parent 916 | if ($parent === $this->parent) { 917 | return $this; 918 | } 919 | 920 | // remove from old parent 921 | if (null !== $this->parent) { 922 | $this->parent->removePage($this); 923 | } 924 | 925 | // set new parent 926 | $this->parent = $parent; 927 | 928 | // add to parent if page and not already a child 929 | if (null !== $this->parent && ! $this->parent->hasPage($this, false)) { 930 | $this->parent->addPage($this); 931 | } 932 | 933 | return $this; 934 | } 935 | 936 | /** 937 | * Returns parent container 938 | * 939 | * @return AbstractContainer|null parent container or null 940 | */ 941 | public function getParent() 942 | { 943 | return $this->parent; 944 | } 945 | 946 | /** 947 | * Sets the given property 948 | * 949 | * If the given property is native (id, class, title, etc), the matching 950 | * set method will be used. Otherwise, it will be set as a custom property. 951 | * 952 | * @param string $property property name 953 | * @param mixed $value value to set 954 | * @return AbstractPage fluent interface, returns self 955 | * @throws Exception\InvalidArgumentException if property name is invalid 956 | */ 957 | public function set($property, $value) 958 | { 959 | if (! is_string($property) || empty($property)) { 960 | throw new Exception\InvalidArgumentException( 961 | 'Invalid argument: $property must be a non-empty string' 962 | ); 963 | } 964 | 965 | $method = 'set' . static::normalizePropertyName($property); 966 | 967 | if ($method != 'setOptions' && method_exists($this, $method) 968 | ) { 969 | $this->$method($value); 970 | } else { 971 | $this->properties[$property] = $value; 972 | } 973 | 974 | return $this; 975 | } 976 | 977 | /** 978 | * Returns the value of the given property 979 | * 980 | * If the given property is native (id, class, title, etc), the matching 981 | * get method will be used. Otherwise, it will return the matching custom 982 | * property, or null if not found. 983 | * 984 | * @param string $property property name 985 | * @return mixed the property's value or null 986 | * @throws Exception\InvalidArgumentException if property name is invalid 987 | */ 988 | public function get($property) 989 | { 990 | if (! is_string($property) || empty($property)) { 991 | throw new Exception\InvalidArgumentException( 992 | 'Invalid argument: $property must be a non-empty string' 993 | ); 994 | } 995 | 996 | $method = 'get' . static::normalizePropertyName($property); 997 | 998 | if (method_exists($this, $method)) { 999 | return $this->$method(); 1000 | } elseif (isset($this->properties[$property])) { 1001 | return $this->properties[$property]; 1002 | } 1003 | 1004 | return; 1005 | } 1006 | 1007 | // Magic overloads: 1008 | 1009 | /** 1010 | * Sets a custom property 1011 | * 1012 | * Magic overload for enabling $page->propname = $value. 1013 | * 1014 | * @param string $name property name 1015 | * @param mixed $value value to set 1016 | * @return void 1017 | * @throws Exception\InvalidArgumentException if property name is invalid 1018 | */ 1019 | public function __set($name, $value) 1020 | { 1021 | $this->set($name, $value); 1022 | } 1023 | 1024 | /** 1025 | * Returns a property, or null if it doesn't exist 1026 | * 1027 | * Magic overload for enabling $page->propname. 1028 | * 1029 | * @param string $name property name 1030 | * @return mixed property value or null 1031 | * @throws Exception\InvalidArgumentException if property name is invalid 1032 | */ 1033 | public function __get($name) 1034 | { 1035 | return $this->get($name); 1036 | } 1037 | 1038 | /** 1039 | * Checks if a property is set 1040 | * 1041 | * Magic overload for enabling isset($page->propname). 1042 | * 1043 | * Returns true if the property is native (id, class, title, etc), and 1044 | * true or false if it's a custom property (depending on whether the 1045 | * property actually is set). 1046 | * 1047 | * @param string $name property name 1048 | * @return bool whether the given property exists 1049 | */ 1050 | public function __isset($name) 1051 | { 1052 | $method = 'get' . static::normalizePropertyName($name); 1053 | if (method_exists($this, $method)) { 1054 | return true; 1055 | } 1056 | 1057 | return isset($this->properties[$name]); 1058 | } 1059 | 1060 | /** 1061 | * Unsets the given custom property 1062 | * 1063 | * Magic overload for enabling unset($page->propname). 1064 | * 1065 | * @param string $name property name 1066 | * @return void 1067 | * @throws Exception\InvalidArgumentException if the property is native 1068 | */ 1069 | public function __unset($name) 1070 | { 1071 | $method = 'set' . static::normalizePropertyName($name); 1072 | if (method_exists($this, $method)) { 1073 | throw new Exception\InvalidArgumentException( 1074 | sprintf( 1075 | 'Unsetting native property "%s" is not allowed', 1076 | $name 1077 | ) 1078 | ); 1079 | } 1080 | 1081 | if (isset($this->properties[$name])) { 1082 | unset($this->properties[$name]); 1083 | } 1084 | } 1085 | 1086 | /** 1087 | * Returns page label 1088 | * 1089 | * Magic overload for enabling echo $page. 1090 | * 1091 | * @return string page label 1092 | */ 1093 | public function __toString() 1094 | { 1095 | return $this->label; 1096 | } 1097 | 1098 | // Public methods: 1099 | 1100 | /** 1101 | * Adds a forward relation to the page 1102 | * 1103 | * @param string $relation relation name (e.g. alternate, glossary, 1104 | * canonical, etc) 1105 | * @param mixed $value value to set for relation 1106 | * @return AbstractPage fluent interface, returns self 1107 | */ 1108 | public function addRel($relation, $value) 1109 | { 1110 | if (is_string($relation)) { 1111 | $this->rel[$relation] = $value; 1112 | } 1113 | return $this; 1114 | } 1115 | 1116 | /** 1117 | * Adds a reverse relation to the page 1118 | * 1119 | * @param string $relation relation name (e.g. alternate, glossary, 1120 | * canonical, etc) 1121 | * @param mixed $value value to set for relation 1122 | * @return AbstractPage fluent interface, returns self 1123 | */ 1124 | public function addRev($relation, $value) 1125 | { 1126 | if (is_string($relation)) { 1127 | $this->rev[$relation] = $value; 1128 | } 1129 | return $this; 1130 | } 1131 | 1132 | /** 1133 | * Removes a forward relation from the page 1134 | * 1135 | * @param string $relation name of relation to remove 1136 | * @return AbstractPage fluent interface, returns self 1137 | */ 1138 | public function removeRel($relation) 1139 | { 1140 | if (isset($this->rel[$relation])) { 1141 | unset($this->rel[$relation]); 1142 | } 1143 | 1144 | return $this; 1145 | } 1146 | 1147 | /** 1148 | * Removes a reverse relation from the page 1149 | * 1150 | * @param string $relation name of relation to remove 1151 | * @return AbstractPage fluent interface, returns self 1152 | */ 1153 | public function removeRev($relation) 1154 | { 1155 | if (isset($this->rev[$relation])) { 1156 | unset($this->rev[$relation]); 1157 | } 1158 | 1159 | return $this; 1160 | } 1161 | 1162 | /** 1163 | * Returns an array containing the defined forward relations 1164 | * 1165 | * @return array defined forward relations 1166 | */ 1167 | public function getDefinedRel() 1168 | { 1169 | return array_keys($this->rel); 1170 | } 1171 | 1172 | /** 1173 | * Returns an array containing the defined reverse relations 1174 | * 1175 | * @return array defined reverse relations 1176 | */ 1177 | public function getDefinedRev() 1178 | { 1179 | return array_keys($this->rev); 1180 | } 1181 | 1182 | /** 1183 | * Returns custom properties as an array 1184 | * 1185 | * @return array an array containing custom properties 1186 | */ 1187 | public function getCustomProperties() 1188 | { 1189 | return $this->properties; 1190 | } 1191 | 1192 | /** 1193 | * Returns a hash code value for the page 1194 | * 1195 | * @return string a hash code value for this page 1196 | */ 1197 | final public function hashCode() 1198 | { 1199 | return spl_object_hash($this); 1200 | } 1201 | 1202 | /** 1203 | * Returns an array representation of the page 1204 | * 1205 | * @return array associative array containing all page properties 1206 | */ 1207 | public function toArray() 1208 | { 1209 | return array_merge($this->getCustomProperties(), [ 1210 | 'label' => $this->getLabel(), 1211 | 'fragment' => $this->getFragment(), 1212 | 'id' => $this->getId(), 1213 | 'class' => $this->getClass(), 1214 | 'title' => $this->getTitle(), 1215 | 'target' => $this->getTarget(), 1216 | 'rel' => $this->getRel(), 1217 | 'rev' => $this->getRev(), 1218 | 'order' => $this->getOrder(), 1219 | 'resource' => $this->getResource(), 1220 | 'privilege' => $this->getPrivilege(), 1221 | 'permission' => $this->getPermission(), 1222 | 'active' => $this->isActive(), 1223 | 'visible' => $this->isVisible(), 1224 | 'type' => get_class($this), 1225 | 'pages' => parent::toArray(), 1226 | ]); 1227 | } 1228 | 1229 | // Internal methods: 1230 | 1231 | /** 1232 | * Normalizes a property name 1233 | * 1234 | * @param string $property property name to normalize 1235 | * @return string normalized property name 1236 | */ 1237 | protected static function normalizePropertyName($property) 1238 | { 1239 | return str_replace(' ', '', ucwords(str_replace('_', ' ', $property))); 1240 | } 1241 | 1242 | // Abstract methods: 1243 | 1244 | /** 1245 | * Returns href for this page 1246 | * 1247 | * @return string the page's href 1248 | */ 1249 | abstract public function getHref(); 1250 | } 1251 | -------------------------------------------------------------------------------- /src/Page/Mvc.php: -------------------------------------------------------------------------------- 1 | active) { 132 | $reqParams = []; 133 | if ($this->routeMatch instanceof RouteMatch || $this->routeMatch instanceof MvcRouter\RouteMatch) { 134 | $reqParams = $this->routeMatch->getParams(); 135 | 136 | if (isset($reqParams[self::ORIGINAL_CONTROLLER])) { 137 | $reqParams['controller'] = $reqParams[self::ORIGINAL_CONTROLLER]; 138 | } 139 | 140 | $pageParams = $this->params; 141 | if (null !== $this->controller) { 142 | $pageParams['controller'] = $this->controller; 143 | } 144 | if (null !== $this->action) { 145 | $pageParams['action'] = $this->action; 146 | } 147 | 148 | if (null !== $this->getRoute()) { 149 | if ($this->routeMatch->getMatchedRouteName() === $this->getRoute() 150 | && (count(array_intersect_assoc($reqParams, $pageParams)) == count($pageParams)) 151 | ) { 152 | $this->active = true; 153 | return $this->active; 154 | } else { 155 | return parent::isActive($recursive); 156 | } 157 | } 158 | } 159 | 160 | $pageParams = $this->params; 161 | 162 | if (null !== $this->controller) { 163 | $pageParams['controller'] = $this->controller; 164 | } else { 165 | /** 166 | * @todo In ZF1, this was configurable and pulled from the front controller 167 | */ 168 | $pageParams['controller'] = 'index'; 169 | } 170 | 171 | if (null !== $this->action) { 172 | $pageParams['action'] = $this->action; 173 | } else { 174 | /** 175 | * @todo In ZF1, this was configurable and pulled from the front controller 176 | */ 177 | $pageParams['action'] = 'index'; 178 | } 179 | 180 | if (count(array_intersect_assoc($reqParams, $pageParams)) == count($pageParams)) { 181 | $this->active = true; 182 | return true; 183 | } 184 | } 185 | 186 | return parent::isActive($recursive); 187 | } 188 | 189 | /** 190 | * Returns href for this page 191 | * 192 | * This method uses {@link RouteStackInterface} to assemble 193 | * the href based on the page's properties. 194 | * 195 | * @see RouteStackInterface 196 | * @return string page href 197 | * @throws Exception\DomainException if no router is set 198 | */ 199 | public function getHref() 200 | { 201 | if ($this->hrefCache) { 202 | return $this->hrefCache; 203 | } 204 | 205 | $router = $this->router; 206 | if (null === $router) { 207 | $router = static::$defaultRouter; 208 | } 209 | 210 | if (! $router instanceof RouteStackInterface && ! $router instanceof MvcRouter\RouteStackInterface) { 211 | throw new Exception\DomainException( 212 | __METHOD__ 213 | . ' cannot execute as no Zend\Router\RouteStackInterface instance is composed' 214 | ); 215 | } 216 | 217 | if ($this->useRouteMatch() && $this->getRouteMatch()) { 218 | $rmParams = $this->getRouteMatch()->getParams(); 219 | 220 | if (isset($rmParams[self::ORIGINAL_CONTROLLER])) { 221 | $rmParams['controller'] = $rmParams[self::ORIGINAL_CONTROLLER]; 222 | unset($rmParams[self::ORIGINAL_CONTROLLER]); 223 | } 224 | 225 | if (isset($rmParams[self::MODULE_NAMESPACE])) { 226 | unset($rmParams[self::MODULE_NAMESPACE]); 227 | } 228 | 229 | $params = array_merge($rmParams, $this->getParams()); 230 | } else { 231 | $params = $this->getParams(); 232 | } 233 | 234 | 235 | if (($param = $this->getController()) !== null) { 236 | $params['controller'] = $param; 237 | } 238 | 239 | if (($param = $this->getAction()) !== null) { 240 | $params['action'] = $param; 241 | } 242 | 243 | switch (true) { 244 | case ($this->getRoute() !== null || static::getDefaultRoute() !== null): 245 | $name = ($this->getRoute() !== null) ? $this->getRoute() : static::getDefaultRoute(); 246 | break; 247 | case ($this->getRouteMatch() !== null): 248 | $name = $this->getRouteMatch()->getMatchedRouteName(); 249 | break; 250 | default: 251 | throw new Exception\DomainException('No route name could be found'); 252 | } 253 | 254 | $options = ['name' => $name]; 255 | 256 | // Add the fragment identifier if it is set 257 | $fragment = $this->getFragment(); 258 | if (null !== $fragment) { 259 | $options['fragment'] = $fragment; 260 | } 261 | 262 | if (null !== ($query = $this->getQuery())) { 263 | $options['query'] = $query; 264 | } 265 | 266 | $url = $router->assemble($params, $options); 267 | 268 | return $this->hrefCache = $url; 269 | } 270 | 271 | /** 272 | * Sets action name to use when assembling URL 273 | * 274 | * @see getHref() 275 | * 276 | * @param string $action action name 277 | * @return Mvc fluent interface, returns self 278 | * @throws Exception\InvalidArgumentException if invalid $action is given 279 | */ 280 | public function setAction($action) 281 | { 282 | if (null !== $action && ! is_string($action)) { 283 | throw new Exception\InvalidArgumentException( 284 | 'Invalid argument: $action must be a string or null' 285 | ); 286 | } 287 | 288 | $this->action = $action; 289 | $this->hrefCache = null; 290 | return $this; 291 | } 292 | 293 | /** 294 | * Returns action name to use when assembling URL 295 | * 296 | * @see getHref() 297 | * 298 | * @return string|null action name 299 | */ 300 | public function getAction() 301 | { 302 | return $this->action; 303 | } 304 | 305 | /** 306 | * Sets controller name to use when assembling URL 307 | * 308 | * @see getHref() 309 | * 310 | * @param string|null $controller controller name 311 | * @return Mvc fluent interface, returns self 312 | * @throws Exception\InvalidArgumentException if invalid controller name is given 313 | */ 314 | public function setController($controller) 315 | { 316 | if (null !== $controller && ! is_string($controller)) { 317 | throw new Exception\InvalidArgumentException( 318 | 'Invalid argument: $controller must be a string or null' 319 | ); 320 | } 321 | 322 | $this->controller = $controller; 323 | $this->hrefCache = null; 324 | return $this; 325 | } 326 | 327 | /** 328 | * Returns controller name to use when assembling URL 329 | * 330 | * @see getHref() 331 | * 332 | * @return string|null controller name or null 333 | */ 334 | public function getController() 335 | { 336 | return $this->controller; 337 | } 338 | 339 | /** 340 | * Sets URL query part to use when assembling URL 341 | * 342 | * @see getHref() 343 | * @param array|string|null $query URL query part 344 | * @return self fluent interface, returns self 345 | */ 346 | public function setQuery($query) 347 | { 348 | $this->query = $query; 349 | $this->hrefCache = null; 350 | return $this; 351 | } 352 | 353 | /** 354 | * Returns URL query part to use when assembling URL 355 | * 356 | * @see getHref() 357 | * 358 | * @return array|string|null URL query part (as an array or string) or null 359 | */ 360 | public function getQuery() 361 | { 362 | return $this->query; 363 | } 364 | 365 | /** 366 | * Sets params to use when assembling URL 367 | * 368 | * @see getHref() 369 | * @param array|null $params [optional] page params. Default is null 370 | * which sets no params. 371 | * @return Mvc fluent interface, returns self 372 | */ 373 | public function setParams(array $params = null) 374 | { 375 | $this->params = empty($params) ? [] : $params; 376 | $this->hrefCache = null; 377 | return $this; 378 | } 379 | 380 | /** 381 | * Returns params to use when assembling URL 382 | * 383 | * @see getHref() 384 | * 385 | * @return array page params 386 | */ 387 | public function getParams() 388 | { 389 | return $this->params; 390 | } 391 | 392 | /** 393 | * Sets route name to use when assembling URL 394 | * 395 | * @see getHref() 396 | * 397 | * @param string $route Route name to use when assembling URL. 398 | * @return Mvc Fluent interface, returns self. 399 | * @throws Exception\InvalidArgumentException If invalid $route is given. 400 | */ 401 | public function setRoute($route) 402 | { 403 | if (null !== $route && (! is_string($route) || strlen($route) < 1)) { 404 | throw new Exception\InvalidArgumentException( 405 | 'Invalid argument: $route must be a non-empty string or null' 406 | ); 407 | } 408 | 409 | $this->route = $route; 410 | $this->hrefCache = null; 411 | return $this; 412 | } 413 | 414 | /** 415 | * Returns route name to use when assembling URL 416 | * 417 | * @see getHref() 418 | * 419 | * @return string route name 420 | */ 421 | public function getRoute() 422 | { 423 | return $this->route; 424 | } 425 | 426 | /** 427 | * Get the route match. 428 | * 429 | * @return RouteMatch 430 | */ 431 | public function getRouteMatch() 432 | { 433 | return $this->routeMatch; 434 | } 435 | 436 | /** 437 | * Set route match object from which parameters will be retrieved 438 | * 439 | * @param RouteMatch|MvcRouter\RouteMatch $matches 440 | * @return Mvc fluent interface, returns self 441 | */ 442 | public function setRouteMatch($matches) 443 | { 444 | if (! $matches instanceof RouteMatch && ! $matches instanceof MvcRouter\RouteMatch) { 445 | throw new Exception\InvalidArgumentException(sprintf( 446 | 'RouteMatch passed to %s must be either a %s or a %s instance; received %s', 447 | __METHOD__, 448 | RouteMatch::class, 449 | MvcRouter\RouteMatch::class, 450 | (is_object($router) ? get_class($router) : gettype($router)) 451 | )); 452 | } 453 | $this->routeMatch = $matches; 454 | return $this; 455 | } 456 | 457 | /** 458 | * Get the useRouteMatch flag 459 | * 460 | * @return bool 461 | */ 462 | public function useRouteMatch() 463 | { 464 | return $this->useRouteMatch; 465 | } 466 | 467 | /** 468 | * Set whether the page should use route match params for assembling link uri 469 | * 470 | * @see getHref() 471 | * @param bool $useRouteMatch [optional] 472 | * @return Mvc 473 | */ 474 | public function setUseRouteMatch($useRouteMatch = true) 475 | { 476 | $this->useRouteMatch = (bool) $useRouteMatch; 477 | $this->hrefCache = null; 478 | return $this; 479 | } 480 | 481 | /** 482 | * Get the router. 483 | * 484 | * @return null|RouteStackInterface|MvcRouter\RouteStackInterface 485 | */ 486 | public function getRouter() 487 | { 488 | return $this->router; 489 | } 490 | 491 | /** 492 | * Sets router for assembling URLs 493 | * 494 | * @see getHref() 495 | * 496 | * @param RouteStackInterface|MvcRouter\RouteStackInterface $router Router 497 | * @return Mvc Fluent interface, returns self 498 | */ 499 | public function setRouter($router) 500 | { 501 | if (! $router instanceof RouteStackInterface && ! $router instanceof MvcRouter\RouteStackInterface) { 502 | throw new Exception\InvalidArgumentException(sprintf( 503 | 'Router passed to %s must be either a %s or a %s instance; received %s', 504 | __METHOD__, 505 | RouteStackInterface::class, 506 | MvcRouter\RouteStackInterface::class, 507 | (is_object($router) ? get_class($router) : gettype($router)) 508 | )); 509 | } 510 | $this->router = $router; 511 | return $this; 512 | } 513 | 514 | /** 515 | * Sets the default router for assembling URLs. 516 | * 517 | * @see getHref() 518 | * @param RouteStackInterface $router Router 519 | * @return void 520 | */ 521 | public static function setDefaultRouter($router) 522 | { 523 | static::$defaultRouter = $router; 524 | } 525 | 526 | /** 527 | * Gets the default router for assembling URLs. 528 | * 529 | * @return RouteStackInterface 530 | */ 531 | public static function getDefaultRouter() 532 | { 533 | return static::$defaultRouter; 534 | } 535 | 536 | /** 537 | * Set default route name 538 | * 539 | * @param string $route 540 | * @return void 541 | */ 542 | public static function setDefaultRoute($route) 543 | { 544 | static::$defaultRoute = $route; 545 | } 546 | 547 | /** 548 | * Get default route name 549 | * 550 | * @return string 551 | */ 552 | public static function getDefaultRoute() 553 | { 554 | return static::$defaultRoute; 555 | } 556 | 557 | // Public methods: 558 | 559 | /** 560 | * Returns an array representation of the page 561 | * 562 | * @return array associative array containing all page properties 563 | */ 564 | public function toArray() 565 | { 566 | return array_merge( 567 | parent::toArray(), 568 | [ 569 | 'action' => $this->getAction(), 570 | 'controller' => $this->getController(), 571 | 'params' => $this->getParams(), 572 | 'route' => $this->getRoute(), 573 | 'router' => $this->getRouter(), 574 | 'route_match' => $this->getRouteMatch(), 575 | ] 576 | ); 577 | } 578 | } 579 | -------------------------------------------------------------------------------- /src/Page/Uri.php: -------------------------------------------------------------------------------- 1 | uri = $uri; 51 | return $this; 52 | } 53 | 54 | /** 55 | * Returns URI 56 | * 57 | * @return string 58 | */ 59 | public function getUri() 60 | { 61 | return $this->uri; 62 | } 63 | 64 | /** 65 | * Returns href for this page 66 | * 67 | * Includes the fragment identifier if it is set. 68 | * 69 | * @return string 70 | */ 71 | public function getHref() 72 | { 73 | $uri = $this->getUri(); 74 | 75 | $fragment = $this->getFragment(); 76 | if (null !== $fragment) { 77 | if ('#' == substr($uri, -1)) { 78 | return $uri . $fragment; 79 | } else { 80 | return $uri . '#' . $fragment; 81 | } 82 | } 83 | 84 | return $uri; 85 | } 86 | 87 | /** 88 | * Returns whether page should be considered active or not 89 | * 90 | * This method will compare the page properties against the request uri. 91 | * 92 | * @param bool $recursive 93 | * [optional] whether page should be considered 94 | * active if any child pages are active. Default is 95 | * false. 96 | * @return bool whether page should be considered active or not 97 | */ 98 | public function isActive($recursive = false) 99 | { 100 | if (! $this->active) { 101 | if ($this->getRequest() instanceof Request) { 102 | if ($this->getRequest()->getUri()->getPath() == $this->getUri()) { 103 | $this->active = true; 104 | return true; 105 | } 106 | } 107 | } 108 | 109 | return parent::isActive($recursive); 110 | } 111 | 112 | /** 113 | * Get the request 114 | * 115 | * @return Request 116 | */ 117 | public function getRequest() 118 | { 119 | return $this->request; 120 | } 121 | 122 | /** 123 | * Sets request for assembling URLs 124 | * 125 | * @param Request $request 126 | * @return self Fluent interface, returns self 127 | */ 128 | public function setRequest(Request $request = null) 129 | { 130 | $this->request = $request; 131 | return $this; 132 | } 133 | 134 | /** 135 | * Returns an array representation of the page 136 | * 137 | * @return array 138 | */ 139 | public function toArray() 140 | { 141 | return array_merge( 142 | parent::toArray(), 143 | [ 144 | 'uri' => $this->getUri(), 145 | ] 146 | ); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Service/AbstractNavigationFactory.php: -------------------------------------------------------------------------------- 1 | getPages($container)); 46 | } 47 | 48 | /** 49 | * Create and return a new Navigation instance (v2). 50 | * 51 | * @param ServiceLocatorInterface $container 52 | * @param null|string $name 53 | * @param null|string $requestedName 54 | * @return Navigation 55 | */ 56 | public function createService(ServiceLocatorInterface $container) 57 | { 58 | return $this($container, Navigation::class); 59 | } 60 | 61 | /** 62 | * @abstract 63 | * @return string 64 | */ 65 | abstract protected function getName(); 66 | 67 | /** 68 | * @param ContainerInterface $container 69 | * @return array 70 | * @throws \Zend\Navigation\Exception\InvalidArgumentException 71 | */ 72 | protected function getPages(ContainerInterface $container) 73 | { 74 | if (null === $this->pages) { 75 | $configuration = $container->get('config'); 76 | 77 | if (! isset($configuration['navigation'])) { 78 | throw new Exception\InvalidArgumentException('Could not find navigation configuration key'); 79 | } 80 | if (! isset($configuration['navigation'][$this->getName()])) { 81 | throw new Exception\InvalidArgumentException(sprintf( 82 | 'Failed to find a navigation container by the name "%s"', 83 | $this->getName() 84 | )); 85 | } 86 | 87 | $pages = $this->getPagesFromConfig($configuration['navigation'][$this->getName()]); 88 | $this->pages = $this->preparePages($container, $pages); 89 | } 90 | return $this->pages; 91 | } 92 | 93 | /** 94 | * @param ContainerInterface $container 95 | * @param array|\Zend\Config\Config $pages 96 | * @return null|array 97 | * @throws \Zend\Navigation\Exception\InvalidArgumentException 98 | */ 99 | protected function preparePages(ContainerInterface $container, $pages) 100 | { 101 | $application = $container->get('Application'); 102 | $routeMatch = $application->getMvcEvent()->getRouteMatch(); 103 | $router = $application->getMvcEvent()->getRouter(); 104 | $request = $application->getMvcEvent()->getRequest(); 105 | 106 | // HTTP request is the only one that may be injected 107 | if (! $request instanceof Request) { 108 | $request = null; 109 | } 110 | 111 | return $this->injectComponents($pages, $routeMatch, $router, $request); 112 | } 113 | 114 | /** 115 | * @param string|\Zend\Config\Config|array $config 116 | * @return array|null|\Zend\Config\Config 117 | * @throws \Zend\Navigation\Exception\InvalidArgumentException 118 | */ 119 | protected function getPagesFromConfig($config = null) 120 | { 121 | if (is_string($config)) { 122 | if (! file_exists($config)) { 123 | throw new Exception\InvalidArgumentException(sprintf( 124 | 'Config was a string but file "%s" does not exist', 125 | $config 126 | )); 127 | } 128 | $config = Config\Factory::fromFile($config); 129 | } elseif ($config instanceof Traversable) { 130 | $config = ArrayUtils::iteratorToArray($config); 131 | } elseif (! is_array($config)) { 132 | throw new Exception\InvalidArgumentException( 133 | 'Invalid input, expected array, filename, or Traversable object' 134 | ); 135 | } 136 | 137 | return $config; 138 | } 139 | 140 | /** 141 | * @param array $pages 142 | * @param RouteMatch|MvcRouter\RouteMatch $routeMatch 143 | * @param Router|MvcRouter\RouteStackInterface $router 144 | * @param null|Request $request 145 | * @return array 146 | */ 147 | protected function injectComponents( 148 | array $pages, 149 | $routeMatch = null, 150 | $router = null, 151 | $request = null 152 | ) { 153 | $this->validateRouteMatch($routeMatch); 154 | $this->validateRouter($router); 155 | 156 | foreach ($pages as &$page) { 157 | $hasUri = isset($page['uri']); 158 | $hasMvc = isset($page['action']) || isset($page['controller']) || isset($page['route']); 159 | if ($hasMvc) { 160 | if (! isset($page['routeMatch']) && $routeMatch) { 161 | $page['routeMatch'] = $routeMatch; 162 | } 163 | if (! isset($page['router'])) { 164 | $page['router'] = $router; 165 | } 166 | } elseif ($hasUri) { 167 | if (! isset($page['request'])) { 168 | $page['request'] = $request; 169 | } 170 | } 171 | 172 | if (isset($page['pages'])) { 173 | $page['pages'] = $this->injectComponents($page['pages'], $routeMatch, $router, $request); 174 | } 175 | } 176 | return $pages; 177 | } 178 | 179 | /** 180 | * Validate that a route match argument provided to injectComponents is valid. 181 | * 182 | * @param null|RouteMatch|MvcRouter\RouteMatch 183 | * @return void 184 | * @throws Exception\InvalidArgumentException 185 | */ 186 | private function validateRouteMatch($routeMatch) 187 | { 188 | if (null === $routeMatch) { 189 | return; 190 | } 191 | 192 | if (! $routeMatch instanceof RouteMatch 193 | && ! $routeMatch instanceof MvcRouter\RouteMatch 194 | ) { 195 | throw new Exception\InvalidArgumentException(sprintf( 196 | '%s or %s expected by %s::injectComponents; received %s', 197 | RouteMatch::class, 198 | MvcRouter\RouteMatch::class, 199 | __CLASS__, 200 | (is_object($routeMatch) ? get_class($routeMatch) : gettype($routeMatch)) 201 | )); 202 | } 203 | } 204 | 205 | /** 206 | * Validate that a router argument provided to injectComponents is valid. 207 | * 208 | * @param null|Router|MvcRouter\RouteStackInterface 209 | * @return void 210 | * @throws Exception\InvalidArgumentException 211 | */ 212 | private function validateRouter($router) 213 | { 214 | if (null === $router) { 215 | return; 216 | } 217 | 218 | if (! $router instanceof Router 219 | && ! $router instanceof MvcRouter\RouteStackInterface 220 | ) { 221 | throw new Exception\InvalidArgumentException(sprintf( 222 | '%s or %s expected by %s::injectComponents; received %s', 223 | RouteMatch::class, 224 | MvcRouter\RouteMatch::class, 225 | __CLASS__, 226 | (is_object($router) ? get_class($router) : gettype($router)) 227 | )); 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Service/ConstructedNavigationFactory.php: -------------------------------------------------------------------------------- 1 | config = $config; 30 | } 31 | 32 | /** 33 | * @param ContainerInterface $container 34 | * @return array|null|\Zend\Config\Config 35 | */ 36 | public function getPages(ContainerInterface $container) 37 | { 38 | if (null === $this->pages) { 39 | $this->pages = $this->preparePages($container, $this->getPagesFromConfig($this->config)); 40 | } 41 | return $this->pages; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getName() 48 | { 49 | return 'constructed'; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Service/DefaultNavigationFactory.php: -------------------------------------------------------------------------------- 1 | get('Zend\Navigation\Special') to retrieve a navigation instance with this configuration. 22 | */ 23 | final class NavigationAbstractServiceFactory implements AbstractFactoryInterface 24 | { 25 | /** 26 | * Top-level configuration key indicating navigation configuration 27 | * 28 | * @var string 29 | */ 30 | const CONFIG_KEY = 'navigation'; 31 | 32 | /** 33 | * Service manager factory prefix 34 | * 35 | * @var string 36 | */ 37 | const SERVICE_PREFIX = 'Zend\\Navigation\\'; 38 | 39 | /** 40 | * Navigation configuration 41 | * 42 | * @var array 43 | */ 44 | protected $config; 45 | 46 | /** 47 | * Can we create a navigation by the requested name? (v3) 48 | * 49 | * @param ContainerInterface $container 50 | * @param string $requestedName Name by which service was requested, must 51 | * start with Zend\Navigation\ 52 | * @return bool 53 | */ 54 | public function canCreate(ContainerInterface $container, $requestedName) 55 | { 56 | if (0 !== strpos($requestedName, self::SERVICE_PREFIX)) { 57 | return false; 58 | } 59 | $config = $this->getConfig($container); 60 | 61 | return $this->hasNamedConfig($requestedName, $config); 62 | } 63 | 64 | /** 65 | * Can we create a navigation by the requested name? (v2) 66 | * 67 | * @param ServiceLocatorInterface $container 68 | * @param string $name Normalized name by which service was requested; 69 | * ignored. 70 | * @param string $requestedName Name by which service was requested, must 71 | * start with Zend\Navigation\ 72 | * @return bool 73 | */ 74 | public function canCreateServiceWithName(ServiceLocatorInterface $container, $name, $requestedName) 75 | { 76 | return $this->canCreate($container, $requestedName); 77 | } 78 | 79 | /** 80 | * {@inheritDoc} 81 | * 82 | * @return Navigation 83 | */ 84 | public function __invoke(ContainerInterface $container, $requestedName, array $options = null) 85 | { 86 | $config = $this->getConfig($container); 87 | $factory = new ConstructedNavigationFactory($this->getNamedConfig($requestedName, $config)); 88 | return $factory($container, $requestedName); 89 | } 90 | 91 | /** 92 | * Can we create a navigation by the requested name? (v2) 93 | * 94 | * @param ServiceLocatorInterface $container 95 | * @param string $name Normalized name by which service was requested; 96 | * ignored. 97 | * @param string $requestedName Name by which service was requested, must 98 | * start with Zend\Navigation\ 99 | * @return Navigation 100 | */ 101 | public function createServiceWithName(ServiceLocatorInterface $container, $name, $requestedName) 102 | { 103 | return $this($container, $requestedName); 104 | } 105 | 106 | /** 107 | * Get navigation configuration, if any 108 | * 109 | * @param ContainerInterface $container 110 | * @return array 111 | */ 112 | protected function getConfig(ContainerInterface $container) 113 | { 114 | if ($this->config !== null) { 115 | return $this->config; 116 | } 117 | 118 | if (! $container->has('config')) { 119 | $this->config = []; 120 | return $this->config; 121 | } 122 | 123 | $config = $container->get('config'); 124 | if (! isset($config[self::CONFIG_KEY]) 125 | || ! is_array($config[self::CONFIG_KEY]) 126 | ) { 127 | $this->config = []; 128 | return $this->config; 129 | } 130 | 131 | $this->config = $config[self::CONFIG_KEY]; 132 | return $this->config; 133 | } 134 | 135 | /** 136 | * Extract config name from service name 137 | * 138 | * @param string $name 139 | * @return string 140 | */ 141 | private function getConfigName($name) 142 | { 143 | return substr($name, strlen(self::SERVICE_PREFIX)); 144 | } 145 | 146 | /** 147 | * Does the configuration have a matching named section? 148 | * 149 | * @param string $name 150 | * @param array|\ArrayAccess $config 151 | * @return bool 152 | */ 153 | private function hasNamedConfig($name, $config) 154 | { 155 | $withoutPrefix = $this->getConfigName($name); 156 | 157 | if (isset($config[$withoutPrefix])) { 158 | return true; 159 | } 160 | 161 | if (isset($config[strtolower($withoutPrefix)])) { 162 | return true; 163 | } 164 | 165 | return false; 166 | } 167 | 168 | /** 169 | * Get the matching named configuration section. 170 | * 171 | * @param string $name 172 | * @param array|\ArrayAccess $config 173 | * @return array 174 | */ 175 | private function getNamedConfig($name, $config) 176 | { 177 | $withoutPrefix = $this->getConfigName($name); 178 | 179 | if (isset($config[$withoutPrefix])) { 180 | return $config[$withoutPrefix]; 181 | } 182 | 183 | if (isset($config[strtolower($withoutPrefix)])) { 184 | return $config[strtolower($withoutPrefix)]; 185 | } 186 | 187 | return []; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/View/HelperConfig.php: -------------------------------------------------------------------------------- 1 | [], 32 | 'aliases' => [ 33 | 'navigation' => NavigationHelper::class, 34 | 'Navigation' => NavigationHelper::class, 35 | ], 36 | 'delegators' => [], 37 | 'factories' => [ 38 | NavigationHelper::class => NavigationHelperFactory::class, 39 | 'zendviewhelpernavigation' => NavigationHelperFactory::class, 40 | ], 41 | 'initializers' => [], 42 | 'invokables' => [], 43 | 'lazy_services' => [], 44 | 'services' => [], 45 | 'shared' => [], 46 | ]; 47 | 48 | /** 49 | * Navigation helper delegator factory. 50 | * 51 | * @var callable 52 | */ 53 | protected $navigationDelegatorFactory; 54 | 55 | /** 56 | * Constructor. 57 | * 58 | * Ensure incoming configuration is *merged* with the defaults defined. 59 | * 60 | * @param array 61 | */ 62 | public function __construct(array $config = []) 63 | { 64 | $this->mergeConfig($config); 65 | } 66 | 67 | /** 68 | * Configure the provided container. 69 | * 70 | * Merges navigation_helpers configuration from the parent containers 71 | * config service with the configuration in this class, and uses that to 72 | * configure the provided service container (which should be the zend-view 73 | * `HelperPluginManager`). with the service locator instance. 74 | * 75 | * Before configuring he provided container, it also adds a delegator 76 | * factory for the `Navigation` helper; the delegator uses the configuration 77 | * from this class to seed the `PluginManager` used by the `NavigationHelper`, 78 | * ensuring that any overrides provided via configuration are propagated 79 | * to it. 80 | * 81 | * @param ServiceManager $serviceManager 82 | * @return ServiceManager 83 | */ 84 | public function configureServiceManager(ServiceManager $container) 85 | { 86 | $services = $this->getParentContainer($container); 87 | 88 | if ($services->has('config')) { 89 | $this->mergeHelpersFromConfiguration($services->get('config')); 90 | } 91 | 92 | $this->injectNavigationDelegatorFactory(method_exists($container, 'configure')); 93 | 94 | parent::configureServiceManager($container); 95 | 96 | return $container; 97 | } 98 | 99 | /** 100 | * Merge an array of configuration with the settings already present. 101 | * 102 | * Processes invokables as invokable factories and optionally additional 103 | * aliases. 104 | * 105 | * @param array $config 106 | * @return void 107 | */ 108 | private function mergeConfig(array $config) 109 | { 110 | if (isset($config['invokables'])) { 111 | $config = $this->processInvokables($config['invokables'], $config); 112 | } 113 | 114 | foreach ($config as $type => $services) { 115 | if (isset($this->config[$type])) { 116 | $this->config[$type] = ArrayUtils::merge($this->config[$type], $services); 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * Merge navigation helper configuration with default configuration. 123 | * 124 | * @param array|Traversable $config 125 | * @return void 126 | */ 127 | private function mergeHelpersFromConfiguration($config) 128 | { 129 | if ($config instanceof Traversable) { 130 | $config = iterator_to_array($config); 131 | } 132 | 133 | if (! isset($config['navigation_helpers']) 134 | || (! is_array($config['navigation_helpers']) && ! $config['navigation_helpers'] instanceof Traversable) 135 | ) { 136 | return; 137 | } 138 | 139 | $this->mergeConfig($config['navigation_helpers']); 140 | } 141 | 142 | /** 143 | * Retrieve the parent container from the plugin manager, if possible. 144 | * 145 | * @param ServiceManager $container 146 | * @return ServiceManager 147 | */ 148 | private function getParentContainer(ServiceManager $container) 149 | { 150 | // We need the parent container in order to retrieve the config 151 | // service. We should likely revisit how this is done in the future. 152 | // 153 | // v3: 154 | if (method_exists($container, 'configure')) { 155 | $r = new ReflectionProperty($container, 'creationContext'); 156 | $r->setAccessible(true); 157 | return $r->getValue($container) ?: $container; 158 | } 159 | 160 | // v2: 161 | return $container->getServiceLocator() ?: $container; 162 | } 163 | 164 | /** 165 | * Normalizes a factory service name for use with zend-servicemanager v2. 166 | * 167 | * @param string $name 168 | * @return string 169 | */ 170 | private function normalizeNameForV2($name) 171 | { 172 | return strtolower(strtr($name, ['-' => '', '_' => '', ' ' => '', '\\' => '', '/' => ''])); 173 | } 174 | 175 | /** 176 | * Process invokables in order to seed aliases and factories. 177 | * 178 | * @param array $invokables Array of invokables defined 179 | * @param array $config All service configuration 180 | * @return array Array of all service configuration 181 | */ 182 | private function processInvokables(array $invokables, array $config) 183 | { 184 | if (! isset($config['aliases'])) { 185 | $config['aliases'] = []; 186 | } 187 | 188 | if (! isset($config['factories'])) { 189 | $config['factories'] = []; 190 | } 191 | 192 | foreach ($invokables as $name => $class) { 193 | $config['factories'][$class] = InvokableFactory::class; 194 | $config['factories'][$this->normalizeNameForV2($class)] = InvokableFactory::class; 195 | 196 | if ($name === $class) { 197 | continue; 198 | } 199 | 200 | $config['aliases'][$name] = $class; 201 | } 202 | 203 | unset($config['invokables']); 204 | 205 | return $config; 206 | } 207 | 208 | /** 209 | * Inject the navigation helper delegator factory into the configuration. 210 | * 211 | * @param bool $isV3Container 212 | * @return void 213 | */ 214 | private function injectNavigationDelegatorFactory($isV3Container) 215 | { 216 | $factory = $this->prepareNavigationDelegatorFactory($isV3Container); 217 | 218 | if (isset($this->config['delegators'][NavigationHelperFactory::class]) 219 | && in_array($factory, $this->config['delegators'][NavigationHelperFactory::class], true) 220 | ) { 221 | // Already present 222 | return; 223 | } 224 | 225 | // Inject the delegator factory 226 | $this->config['delegators'][NavigationHelper::class][] = $factory; 227 | $this->config['delegators']['zendviewhelpernavigation'][] = $factory; 228 | } 229 | 230 | /** 231 | * Return a delegator factory that configures the navigation plugin manager 232 | * with the configuration in this class. 233 | * 234 | * @param bool $isV3Container 235 | * @return callable 236 | */ 237 | private function prepareNavigationDelegatorFactory($isV3Container) 238 | { 239 | if (isset($this->navigationDelegatorFactory)) { 240 | return $this->navigationDelegatorFactory; 241 | } 242 | 243 | $this->navigationDelegatorFactory = $isV3Container 244 | ? $this->prepareV3NavigationDelegatorFactory($this->config) 245 | : $this->prepareV2NavigationDelegatorFactory($this->config); 246 | 247 | return $this->navigationDelegatorFactory; 248 | } 249 | 250 | /** 251 | * Return a delegator factory compatible with v2 252 | * 253 | * @param array $config Configuration to use when configuring the 254 | * navigation plugin manager. 255 | * @return callable 256 | */ 257 | private function prepareV2NavigationDelegatorFactory(array $config) 258 | { 259 | return function ($container, $canonicalName, $requestedName, $callback) use ($config) { 260 | $helper = $callback(); 261 | 262 | $pluginManager = $helper->getPluginManager(); 263 | (new Config($config))->configureServiceManager($pluginManager); 264 | 265 | return $helper; 266 | }; 267 | } 268 | 269 | /** 270 | * Return a delegator factory compatible with v3 271 | * 272 | * @param array $config Configuration to use when configuring the 273 | * navigation plugin manager. 274 | * @return callable 275 | */ 276 | private function prepareV3NavigationDelegatorFactory(array $config) 277 | { 278 | return function ($container, $name, $callback, $options) use ($config) { 279 | $helper = $callback(); 280 | 281 | $pluginManager = $helper->getPluginManager(); 282 | (new Config($config))->configureServiceManager($pluginManager); 283 | 284 | return $helper; 285 | }; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/View/NavigationHelperFactory.php: -------------------------------------------------------------------------------- 1 | setServiceLocator($this->getApplicationServicesFromContainer($container)); 32 | return $helper; 33 | } 34 | 35 | /** 36 | * Create and return a navigation helper instance. (v2) 37 | * 38 | * @param ServiceLocatorInterface $container 39 | * @param null|string $name 40 | * @param string $requestedName 41 | * @return NavigationHelper 42 | */ 43 | public function createService( 44 | ServiceLocatorInterface $container, 45 | $name = null, 46 | $requestedName = NavigationHelper::class 47 | ) { 48 | return $this($container, $requestedName); 49 | } 50 | 51 | /** 52 | * Retrieve the application (parent) services from the container, if possible. 53 | * 54 | * @param ContainerInterface $container 55 | * @return ContainerInterface 56 | */ 57 | private function getApplicationServicesFromContainer(ContainerInterface $container) 58 | { 59 | // v3 60 | if (method_exists($container, 'configure')) { 61 | $r = new ReflectionProperty($container, 'creationContext'); 62 | $r->setAccessible(true); 63 | return $r->getValue($container) ?: $container; 64 | } 65 | 66 | // v2 67 | return $container->getServiceLocator() ?: $container; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/View/ViewHelperManagerDelegatorFactory.php: -------------------------------------------------------------------------------- 1 | configureServiceManager($viewHelpers); 32 | return $viewHelpers; 33 | } 34 | 35 | /** 36 | * {@inheritDoc} 37 | * 38 | * @return \Zend\View\HelperPluginManager 39 | */ 40 | public function createDelegatorWithName(ServiceLocatorInterface $container, $name, $requestedName, $callback) 41 | { 42 | return $this($container, $requestedName, $callback); 43 | } 44 | } 45 | --------------------------------------------------------------------------------