├── 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 | [](https://secure.travis-ci.org/zendframework/zend-navigation)
8 | [](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 |
--------------------------------------------------------------------------------