1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\Navigation\Page;
11
12use Zend\Mvc\ModuleRouteListener;
13use Zend\Mvc\Router\RouteMatch;
14use Zend\Mvc\Router\RouteStackInterface;
15use Zend\Navigation\Exception;
16
17/**
18 * Represents a page that is defined using controller, action, route
19 * name and route params to assemble the href
20 */
21class Mvc extends AbstractPage
22{
23    /**
24     * Action name to use when assembling URL
25     *
26     * @var string
27     */
28    protected $action;
29
30    /**
31     * Controller name to use when assembling URL
32     *
33     * @var string
34     */
35    protected $controller;
36
37    /**
38     * URL query part to use when assembling URL
39     *
40     * @var array|string
41     */
42    protected $query;
43
44    /**
45     * Params to use when assembling URL
46     *
47     * @see getHref()
48     * @var array
49     */
50    protected $params = array();
51
52    /**
53     * RouteInterface name to use when assembling URL
54     *
55     * @see getHref()
56     * @var string
57     */
58    protected $route;
59
60    /**
61     * Cached href
62     *
63     * The use of this variable minimizes execution time when getHref() is
64     * called more than once during the lifetime of a request. If a property
65     * is updated, the cache is invalidated.
66     *
67     * @var string
68     */
69    protected $hrefCache;
70
71    /**
72     * RouteInterface matches; used for routing parameters and testing validity
73     *
74     * @var RouteMatch
75     */
76    protected $routeMatch;
77
78    /**
79     * If true and set routeMatch than getHref will use routeMatch params
80     * to assemble uri
81     * @var bool
82     */
83    protected $useRouteMatch = false;
84
85    /**
86     * Router for assembling URLs
87     *
88     * @see getHref()
89     * @var RouteStackInterface
90     */
91    protected $router = null;
92
93    /**
94     * Default router to be used if router is not given.
95     *
96     * @see getHref()
97     *
98     * @var RouteStackInterface
99     */
100    protected static $defaultRouter = null;
101
102    /**
103     * Default route name
104     *
105     * @var string
106     */
107    protected static $defaultRoute = null;
108
109    // Accessors:
110
111    /**
112     * Returns whether page should be considered active or not
113     *
114     * This method will compare the page properties against the route matches
115     * composed in the object.
116     *
117     * @param  bool $recursive  [optional] whether page should be considered
118     *                          active if any child pages are active. Default is
119     *                          false.
120     * @return bool             whether page should be considered active or not
121     */
122    public function isActive($recursive = false)
123    {
124        if (!$this->active) {
125            $reqParams = array();
126            if ($this->routeMatch instanceof RouteMatch) {
127                $reqParams  = $this->routeMatch->getParams();
128
129                if (isset($reqParams[ModuleRouteListener::ORIGINAL_CONTROLLER])) {
130                    $reqParams['controller'] = $reqParams[ModuleRouteListener::ORIGINAL_CONTROLLER];
131                }
132
133                $pageParams   = $this->params;
134                if (null !== $this->controller) {
135                    $pageParams['controller'] = $this->controller;
136                }
137                if (null !== $this->action) {
138                    $pageParams['action'] = $this->action;
139                }
140
141                if (null !== $this->getRoute()) {
142                    if (
143                        $this->routeMatch->getMatchedRouteName() === $this->getRoute()
144                        && (count(array_intersect_assoc($reqParams, $pageParams)) == count($pageParams))
145                    ) {
146                        $this->active = true;
147                        return $this->active;
148                    } else {
149                        return parent::isActive($recursive);
150                    }
151                }
152            }
153
154            $pageParams = $this->params;
155
156            if (null !== $this->controller) {
157                $pageParams['controller'] = $this->controller;
158            } else {
159                /**
160                 * @todo In ZF1, this was configurable and pulled from the front controller
161                 */
162                $pageParams['controller'] = 'index';
163            }
164
165            if (null !== $this->action) {
166                $pageParams['action'] = $this->action;
167            } else {
168                /**
169                 * @todo In ZF1, this was configurable and pulled from the front controller
170                 */
171                $pageParams['action'] = 'index';
172            }
173
174            if (count(array_intersect_assoc($reqParams, $pageParams)) == count($pageParams)) {
175                $this->active = true;
176                return true;
177            }
178        }
179
180        return parent::isActive($recursive);
181    }
182
183    /**
184     * Returns href for this page
185     *
186     * This method uses {@link RouteStackInterface} to assemble
187     * the href based on the page's properties.
188     *
189     * @see RouteStackInterface
190     * @return string  page href
191     * @throws Exception\DomainException if no router is set
192     */
193    public function getHref()
194    {
195        if ($this->hrefCache) {
196            return $this->hrefCache;
197        }
198
199        $router = $this->router;
200        if (null === $router) {
201            $router = static::$defaultRouter;
202        }
203
204        if (!$router instanceof RouteStackInterface) {
205            throw new Exception\DomainException(
206                __METHOD__
207                . ' cannot execute as no Zend\Mvc\Router\RouteStackInterface instance is composed'
208            );
209        }
210
211        if ($this->useRouteMatch() && $this->getRouteMatch()) {
212            $rmParams = $this->getRouteMatch()->getParams();
213
214            if (isset($rmParams[ModuleRouteListener::ORIGINAL_CONTROLLER])) {
215                $rmParams['controller'] = $rmParams[ModuleRouteListener::ORIGINAL_CONTROLLER];
216                unset($rmParams[ModuleRouteListener::ORIGINAL_CONTROLLER]);
217            }
218
219            if (isset($rmParams[ModuleRouteListener::MODULE_NAMESPACE])) {
220                unset($rmParams[ModuleRouteListener::MODULE_NAMESPACE]);
221            }
222
223            $params = array_merge($rmParams, $this->getParams());
224        } else {
225            $params = $this->getParams();
226        }
227
228
229        if (($param = $this->getController()) !== null) {
230            $params['controller'] = $param;
231        }
232
233        if (($param = $this->getAction()) !== null) {
234            $params['action'] = $param;
235        }
236
237        switch (true) {
238            case ($this->getRoute() !== null || static::getDefaultRoute() !== null):
239                $name = ($this->getRoute() !== null) ? $this->getRoute() : static::getDefaultRoute();
240                break;
241            case ($this->getRouteMatch() !== null):
242                $name = $this->getRouteMatch()->getMatchedRouteName();
243                break;
244            default:
245                throw new Exception\DomainException('No route name could be found');
246        }
247
248        $options = array('name' => $name);
249
250        // Add the fragment identifier if it is set
251        $fragment = $this->getFragment();
252        if (null !== $fragment) {
253            $options['fragment'] = $fragment;
254        }
255
256        if (null !== ($query = $this->getQuery())) {
257            $options['query'] = $query;
258        }
259
260        $url = $router->assemble($params, $options);
261
262        return $this->hrefCache = $url;
263    }
264
265    /**
266     * Sets action name to use when assembling URL
267     *
268     * @see getHref()
269     *
270     * @param  string $action             action name
271     * @return Mvc   fluent interface, returns self
272     * @throws Exception\InvalidArgumentException  if invalid $action is given
273     */
274    public function setAction($action)
275    {
276        if (null !== $action && !is_string($action)) {
277            throw new Exception\InvalidArgumentException(
278                'Invalid argument: $action must be a string or null'
279            );
280        }
281
282        $this->action    = $action;
283        $this->hrefCache = null;
284        return $this;
285    }
286
287    /**
288     * Returns action name to use when assembling URL
289     *
290     * @see getHref()
291     *
292     * @return string|null  action name
293     */
294    public function getAction()
295    {
296        return $this->action;
297    }
298
299    /**
300     * Sets controller name to use when assembling URL
301     *
302     * @see getHref()
303     *
304     * @param  string|null $controller    controller name
305     * @return Mvc   fluent interface, returns self
306     * @throws Exception\InvalidArgumentException  if invalid controller name is given
307     */
308    public function setController($controller)
309    {
310        if (null !== $controller && !is_string($controller)) {
311            throw new Exception\InvalidArgumentException(
312                'Invalid argument: $controller must be a string or null'
313            );
314        }
315
316        $this->controller = $controller;
317        $this->hrefCache  = null;
318        return $this;
319    }
320
321    /**
322     * Returns controller name to use when assembling URL
323     *
324     * @see getHref()
325     *
326     * @return string|null  controller name or null
327     */
328    public function getController()
329    {
330        return $this->controller;
331    }
332
333    /**
334     * Sets URL query part to use when assembling URL
335     *
336     * @see getHref()
337     * @param  array|string|null $query    URL query part
338     * @return self   fluent interface, returns self
339     */
340    public function setQuery($query)
341    {
342        $this->query      = $query;
343        $this->hrefCache  = null;
344        return $this;
345    }
346
347    /**
348     * Returns URL query part to use when assembling URL
349     *
350     * @see getHref()
351     *
352     * @return array|string|null  URL query part (as an array or string) or null
353     */
354    public function getQuery()
355    {
356        return $this->query;
357    }
358
359    /**
360     * Sets params to use when assembling URL
361     *
362     * @see getHref()
363     * @param  array|null $params [optional] page params. Default is null
364     *                            which sets no params.
365     * @return Mvc  fluent interface, returns self
366     */
367    public function setParams(array $params = null)
368    {
369        $this->params = empty($params) ? array() : $params;
370        $this->hrefCache = null;
371        return $this;
372    }
373
374    /**
375     * Returns params to use when assembling URL
376     *
377     * @see getHref()
378     *
379     * @return array  page params
380     */
381    public function getParams()
382    {
383        return $this->params;
384    }
385
386    /**
387     * Sets route name to use when assembling URL
388     *
389     * @see getHref()
390     *
391     * @param  string $route              route name to use when assembling URL
392     * @return Mvc   fluent interface, returns self
393     * @throws Exception\InvalidArgumentException  if invalid $route is given
394     */
395    public function setRoute($route)
396    {
397        if (null !== $route && (!is_string($route) || strlen($route) < 1)) {
398            throw new Exception\InvalidArgumentException(
399                'Invalid argument: $route must be a non-empty string or null'
400            );
401        }
402
403        $this->route     = $route;
404        $this->hrefCache = null;
405        return $this;
406    }
407
408    /**
409     * Returns route name to use when assembling URL
410     *
411     * @see getHref()
412     *
413     * @return string  route name
414     */
415    public function getRoute()
416    {
417        return $this->route;
418    }
419
420    /**
421     * Get the route match.
422     *
423     * @return \Zend\Mvc\Router\RouteMatch
424     */
425    public function getRouteMatch()
426    {
427        return $this->routeMatch;
428    }
429
430    /**
431     * Set route match object from which parameters will be retrieved
432     *
433     * @param  RouteMatch $matches
434     * @return Mvc fluent interface, returns self
435     */
436    public function setRouteMatch(RouteMatch $matches)
437    {
438        $this->routeMatch = $matches;
439        return $this;
440    }
441
442    /**
443     * Get the useRouteMatch flag
444     *
445     * @return bool
446     */
447    public function useRouteMatch()
448    {
449        return $this->useRouteMatch;
450    }
451
452    /**
453     * Set whether the page should use route match params for assembling link uri
454     *
455     * @see getHref()
456     * @param bool $useRouteMatch [optional]
457     * @return Mvc
458     */
459    public function setUseRouteMatch($useRouteMatch = true)
460    {
461        $this->useRouteMatch = (bool) $useRouteMatch;
462        $this->hrefCache = null;
463        return $this;
464    }
465
466    /**
467     * Get the router.
468     *
469     * @return null|RouteStackInterface
470     */
471    public function getRouter()
472    {
473        return $this->router;
474    }
475
476    /**
477     * Sets router for assembling URLs
478     *
479     * @see getHref()
480     *
481     * @param  RouteStackInterface $router Router
482     * @return Mvc    fluent interface, returns self
483     */
484    public function setRouter(RouteStackInterface $router)
485    {
486        $this->router = $router;
487        return $this;
488    }
489
490    /**
491     * Sets the default router for assembling URLs.
492     *
493     * @see getHref()
494     * @param  RouteStackInterface $router Router
495     * @return void
496     */
497    public static function setDefaultRouter($router)
498    {
499        static::$defaultRouter = $router;
500    }
501
502    /**
503     * Gets the default router for assembling URLs.
504     *
505     * @return RouteStackInterface
506     */
507    public static function getDefaultRouter()
508    {
509        return static::$defaultRouter;
510    }
511
512    /**
513     * Set default route name
514     *
515     * @param string $route
516     * @return void
517     */
518    public static function setDefaultRoute($route)
519    {
520        static::$defaultRoute = $route;
521    }
522
523    /**
524     * Get default route name
525     *
526     * @return string
527     */
528    public static function getDefaultRoute()
529    {
530        return static::$defaultRoute;
531    }
532
533    // Public methods:
534
535    /**
536     * Returns an array representation of the page
537     *
538     * @return array  associative array containing all page properties
539     */
540    public function toArray()
541    {
542        return array_merge(
543            parent::toArray(),
544            array(
545                 'action'     => $this->getAction(),
546                 'controller' => $this->getController(),
547                 'params'     => $this->getParams(),
548                 'route'      => $this->getRoute(),
549                 'router'     => $this->getRouter(),
550                 'route_match' => $this->getRouteMatch(),
551            )
552        );
553    }
554}
555