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\View\Helper\Navigation; 11 12use RecursiveIteratorIterator; 13use Zend\EventManager\EventManager; 14use Zend\EventManager\EventManagerAwareInterface; 15use Zend\EventManager\EventManagerInterface; 16use Zend\I18n\Translator\TranslatorInterface as Translator; 17use Zend\I18n\Translator\TranslatorAwareInterface; 18use Zend\Navigation; 19use Zend\Navigation\Page\AbstractPage; 20use Zend\Permissions\Acl; 21use Zend\ServiceManager\ServiceLocatorAwareInterface; 22use Zend\ServiceManager\ServiceLocatorInterface; 23use Zend\View; 24use Zend\View\Exception; 25 26/** 27 * Base class for navigational helpers 28 */ 29abstract class AbstractHelper extends View\Helper\AbstractHtmlElement implements 30 EventManagerAwareInterface, 31 HelperInterface, 32 ServiceLocatorAwareInterface, 33 TranslatorAwareInterface 34{ 35 /** 36 * @var EventManagerInterface 37 */ 38 protected $events; 39 40 /** 41 * @var ServiceLocatorInterface 42 */ 43 protected $serviceLocator; 44 45 /** 46 * AbstractContainer to operate on by default 47 * 48 * @var Navigation\AbstractContainer 49 */ 50 protected $container; 51 52 /** 53 * The minimum depth a page must have to be included when rendering 54 * 55 * @var int 56 */ 57 protected $minDepth; 58 59 /** 60 * The maximum depth a page can have to be included when rendering 61 * 62 * @var int 63 */ 64 protected $maxDepth; 65 66 /** 67 * Indentation string 68 * 69 * @var string 70 */ 71 protected $indent = ''; 72 73 /** 74 * ACL to use when iterating pages 75 * 76 * @var Acl\AclInterface 77 */ 78 protected $acl; 79 80 /** 81 * Whether invisible items should be rendered by this helper 82 * 83 * @var bool 84 */ 85 protected $renderInvisible = false; 86 87 /** 88 * ACL role to use when iterating pages 89 * 90 * @var string|Acl\Role\RoleInterface 91 */ 92 protected $role; 93 94 /** 95 * Whether ACL should be used for filtering out pages 96 * 97 * @var bool 98 */ 99 protected $useAcl = true; 100 101 /** 102 * Translator (optional) 103 * 104 * @var Translator 105 */ 106 protected $translator; 107 108 /** 109 * Translator text domain (optional) 110 * 111 * @var string 112 */ 113 protected $translatorTextDomain = 'default'; 114 115 /** 116 * Whether translator should be used 117 * 118 * @var bool 119 */ 120 protected $translatorEnabled = true; 121 122 /** 123 * Default ACL to use when iterating pages if not explicitly set in the 124 * instance by calling {@link setAcl()} 125 * 126 * @var Acl\AclInterface 127 */ 128 protected static $defaultAcl; 129 130 /** 131 * Default ACL role to use when iterating pages if not explicitly set in the 132 * instance by calling {@link setRole()} 133 * 134 * @var string|Acl\Role\RoleInterface 135 */ 136 protected static $defaultRole; 137 138 /** 139 * Magic overload: Proxy calls to the navigation container 140 * 141 * @param string $method method name in container 142 * @param array $arguments rguments to pass 143 * @return mixed 144 * @throws Navigation\Exception\ExceptionInterface 145 */ 146 public function __call($method, array $arguments = array()) 147 { 148 return call_user_func_array( 149 array($this->getContainer(), $method), 150 $arguments 151 ); 152 } 153 154 /** 155 * Magic overload: Proxy to {@link render()}. 156 * 157 * This method will trigger an E_USER_ERROR if rendering the helper causes 158 * an exception to be thrown. 159 * 160 * Implements {@link HelperInterface::__toString()}. 161 * 162 * @return string 163 */ 164 public function __toString() 165 { 166 try { 167 return $this->render(); 168 } catch (\Exception $e) { 169 $msg = get_class($e) . ': ' . $e->getMessage(); 170 trigger_error($msg, E_USER_ERROR); 171 return ''; 172 } 173 } 174 175 /** 176 * Finds the deepest active page in the given container 177 * 178 * @param Navigation\AbstractContainer $container container to search 179 * @param int|null $minDepth [optional] minimum depth 180 * required for page to be 181 * valid. Default is to use 182 * {@link getMinDepth()}. A 183 * null value means no minimum 184 * depth required. 185 * @param int|null $maxDepth [optional] maximum depth 186 * a page can have to be 187 * valid. Default is to use 188 * {@link getMaxDepth()}. A 189 * null value means no maximum 190 * depth required. 191 * @return array an associative array with 192 * the values 'depth' and 193 * 'page', or an empty array 194 * if not found 195 */ 196 public function findActive($container, $minDepth = null, $maxDepth = -1) 197 { 198 $this->parseContainer($container); 199 if (!is_int($minDepth)) { 200 $minDepth = $this->getMinDepth(); 201 } 202 if ((!is_int($maxDepth) || $maxDepth < 0) && null !== $maxDepth) { 203 $maxDepth = $this->getMaxDepth(); 204 } 205 206 $found = null; 207 $foundDepth = -1; 208 $iterator = new RecursiveIteratorIterator( 209 $container, 210 RecursiveIteratorIterator::CHILD_FIRST 211 ); 212 213 /** @var \Zend\Navigation\Page\AbstractPage $page */ 214 foreach ($iterator as $page) { 215 $currDepth = $iterator->getDepth(); 216 if ($currDepth < $minDepth || !$this->accept($page)) { 217 // page is not accepted 218 continue; 219 } 220 221 if ($page->isActive(false) && $currDepth > $foundDepth) { 222 // found an active page at a deeper level than before 223 $found = $page; 224 $foundDepth = $currDepth; 225 } 226 } 227 228 if (is_int($maxDepth) && $foundDepth > $maxDepth) { 229 while ($foundDepth > $maxDepth) { 230 if (--$foundDepth < $minDepth) { 231 $found = null; 232 break; 233 } 234 235 $found = $found->getParent(); 236 if (!$found instanceof AbstractPage) { 237 $found = null; 238 break; 239 } 240 } 241 } 242 243 if ($found) { 244 return array('page' => $found, 'depth' => $foundDepth); 245 } 246 247 return array(); 248 } 249 250 /** 251 * Verifies container and eventually fetches it from service locator if it is a string 252 * 253 * @param Navigation\AbstractContainer|string|null $container 254 * @throws Exception\InvalidArgumentException 255 */ 256 protected function parseContainer(&$container = null) 257 { 258 if (null === $container) { 259 return; 260 } 261 262 if (is_string($container)) { 263 if (!$this->getServiceLocator()) { 264 throw new Exception\InvalidArgumentException(sprintf( 265 'Attempted to set container with alias "%s" but no ServiceLocator was set', 266 $container 267 )); 268 } 269 270 /** 271 * Load the navigation container from the root service locator 272 * 273 * The navigation container is probably located in Zend\ServiceManager\ServiceManager 274 * and not in the View\HelperPluginManager. If the set service locator is a 275 * HelperPluginManager, access the navigation container via the main service locator. 276 */ 277 $sl = $this->getServiceLocator(); 278 if ($sl instanceof View\HelperPluginManager) { 279 $sl = $sl->getServiceLocator(); 280 } 281 $container = $sl->get($container); 282 return; 283 } 284 285 if (!$container instanceof Navigation\AbstractContainer) { 286 throw new Exception\InvalidArgumentException( 287 'Container must be a string alias or an instance of ' 288 . 'Zend\Navigation\AbstractContainer' 289 ); 290 } 291 } 292 293 // Iterator filter methods: 294 295 /** 296 * Determines whether a page should be accepted when iterating 297 * 298 * Default listener may be 'overridden' by attaching listener to 'isAllowed' 299 * method. Listener must be 'short circuited' if overriding default ACL 300 * listener. 301 * 302 * Rules: 303 * - If a page is not visible it is not accepted, unless RenderInvisible has 304 * been set to true 305 * - If $useAcl is true (default is true): 306 * - Page is accepted if listener returns true, otherwise false 307 * - If page is accepted and $recursive is true, the page 308 * will not be accepted if it is the descendant of a non-accepted page 309 * 310 * @param AbstractPage $page page to check 311 * @param bool $recursive [optional] if true, page will not be 312 * accepted if it is the descendant of 313 * a page that is not accepted. Default 314 * is true 315 * 316 * @return bool Whether page should be accepted 317 */ 318 public function accept(AbstractPage $page, $recursive = true) 319 { 320 $accept = true; 321 322 if (!$page->isVisible(false) && !$this->getRenderInvisible()) { 323 $accept = false; 324 } elseif ($this->getUseAcl()) { 325 $acl = $this->getAcl(); 326 $role = $this->getRole(); 327 $params = array('acl' => $acl, 'page' => $page, 'role' => $role); 328 $accept = $this->isAllowed($params); 329 } 330 331 if ($accept && $recursive) { 332 $parent = $page->getParent(); 333 334 if ($parent instanceof AbstractPage) { 335 $accept = $this->accept($parent, true); 336 } 337 } 338 339 return $accept; 340 } 341 342 /** 343 * Determines whether a page should be allowed given certain parameters 344 * 345 * @param array $params 346 * @return bool 347 */ 348 protected function isAllowed($params) 349 { 350 $results = $this->getEventManager()->trigger(__FUNCTION__, $this, $params); 351 return $results->last(); 352 } 353 354 // Util methods: 355 356 /** 357 * Retrieve whitespace representation of $indent 358 * 359 * @param int|string $indent 360 * @return string 361 */ 362 protected function getWhitespace($indent) 363 { 364 if (is_int($indent)) { 365 $indent = str_repeat(' ', $indent); 366 } 367 368 return (string) $indent; 369 } 370 371 /** 372 * Converts an associative array to a string of tag attributes. 373 * 374 * Overloads {@link View\Helper\AbstractHtmlElement::htmlAttribs()}. 375 * 376 * @param array $attribs an array where each key-value pair is converted 377 * to an attribute name and value 378 * @return string 379 */ 380 protected function htmlAttribs($attribs) 381 { 382 // filter out null values and empty string values 383 foreach ($attribs as $key => $value) { 384 if ($value === null || (is_string($value) && !strlen($value))) { 385 unset($attribs[$key]); 386 } 387 } 388 389 return parent::htmlAttribs($attribs); 390 } 391 392 /** 393 * Returns an HTML string containing an 'a' element for the given page 394 * 395 * @param AbstractPage $page page to generate HTML for 396 * @return string HTML string (<a href="…">Label</a>) 397 */ 398 public function htmlify(AbstractPage $page) 399 { 400 $label = $this->translate($page->getLabel(), $page->getTextDomain()); 401 $title = $this->translate($page->getTitle(), $page->getTextDomain()); 402 403 // get attribs for anchor element 404 $attribs = array( 405 'id' => $page->getId(), 406 'title' => $title, 407 'class' => $page->getClass(), 408 'href' => $page->getHref(), 409 'target' => $page->getTarget() 410 ); 411 412 /** @var \Zend\View\Helper\EscapeHtml $escaper */ 413 $escaper = $this->view->plugin('escapeHtml'); 414 $label = $escaper($label); 415 416 return '<a' . $this->htmlAttribs($attribs) . '>' . $label . '</a>'; 417 } 418 419 /** 420 * Translate a message (for label, title, …) 421 * 422 * @param string $message ID of the message to translate 423 * @param string $textDomain Text domain (category name for the translations) 424 * @return string Translated message 425 */ 426 protected function translate($message, $textDomain = null) 427 { 428 if (is_string($message) && !empty($message)) { 429 if (null !== ($translator = $this->getTranslator())) { 430 if (null === $textDomain) { 431 $textDomain = $this->getTranslatorTextDomain(); 432 } 433 434 return $translator->translate($message, $textDomain); 435 } 436 } 437 438 return $message; 439 } 440 441 /** 442 * Normalize an ID 443 * 444 * Overrides {@link View\Helper\AbstractHtmlElement::normalizeId()}. 445 * 446 * @param string $value 447 * @return string 448 */ 449 protected function normalizeId($value) 450 { 451 $prefix = get_class($this); 452 $prefix = strtolower(trim(substr($prefix, strrpos($prefix, '\\')), '\\')); 453 454 return $prefix . '-' . $value; 455 } 456 457 /** 458 * Sets ACL to use when iterating pages 459 * 460 * Implements {@link HelperInterface::setAcl()}. 461 * 462 * @param Acl\AclInterface $acl ACL object. 463 * @return AbstractHelper 464 */ 465 public function setAcl(Acl\AclInterface $acl = null) 466 { 467 $this->acl = $acl; 468 return $this; 469 } 470 471 /** 472 * Returns ACL or null if it isn't set using {@link setAcl()} or 473 * {@link setDefaultAcl()} 474 * 475 * Implements {@link HelperInterface::getAcl()}. 476 * 477 * @return Acl\AclInterface|null ACL object or null 478 */ 479 public function getAcl() 480 { 481 if ($this->acl === null && static::$defaultAcl !== null) { 482 return static::$defaultAcl; 483 } 484 485 return $this->acl; 486 } 487 488 /** 489 * Checks if the helper has an ACL instance 490 * 491 * Implements {@link HelperInterface::hasAcl()}. 492 * 493 * @return bool 494 */ 495 public function hasAcl() 496 { 497 if ($this->acl instanceof Acl\Acl 498 || static::$defaultAcl instanceof Acl\Acl 499 ) { 500 return true; 501 } 502 503 return false; 504 } 505 506 /** 507 * Set the event manager. 508 * 509 * @param EventManagerInterface $events 510 * @return AbstractHelper 511 */ 512 public function setEventManager(EventManagerInterface $events) 513 { 514 $events->setIdentifiers(array( 515 __CLASS__, 516 get_called_class(), 517 )); 518 519 $this->events = $events; 520 521 $this->setDefaultListeners(); 522 523 return $this; 524 } 525 526 /** 527 * Get the event manager. 528 * 529 * @return EventManagerInterface 530 */ 531 public function getEventManager() 532 { 533 if (null === $this->events) { 534 $this->setEventManager(new EventManager()); 535 } 536 537 return $this->events; 538 } 539 540 /** 541 * Sets navigation container the helper operates on by default 542 * 543 * Implements {@link HelperInterface::setContainer()}. 544 * 545 * @param string|Navigation\AbstractContainer $container Default is null, meaning container will be reset. 546 * @return AbstractHelper 547 */ 548 public function setContainer($container = null) 549 { 550 $this->parseContainer($container); 551 $this->container = $container; 552 553 return $this; 554 } 555 556 /** 557 * Returns the navigation container helper operates on by default 558 * 559 * Implements {@link HelperInterface::getContainer()}. 560 * 561 * If no container is set, a new container will be instantiated and 562 * stored in the helper. 563 * 564 * @return Navigation\AbstractContainer navigation container 565 */ 566 public function getContainer() 567 { 568 if (null === $this->container) { 569 $this->container = new Navigation\Navigation(); 570 } 571 572 return $this->container; 573 } 574 575 /** 576 * Checks if the helper has a container 577 * 578 * Implements {@link HelperInterface::hasContainer()}. 579 * 580 * @return bool 581 */ 582 public function hasContainer() 583 { 584 return null !== $this->container; 585 } 586 587 /** 588 * Set the indentation string for using in {@link render()}, optionally a 589 * number of spaces to indent with 590 * 591 * @param string|int $indent 592 * @return AbstractHelper 593 */ 594 public function setIndent($indent) 595 { 596 $this->indent = $this->getWhitespace($indent); 597 return $this; 598 } 599 600 /** 601 * Returns indentation 602 * 603 * @return string 604 */ 605 public function getIndent() 606 { 607 return $this->indent; 608 } 609 610 /** 611 * Sets the maximum depth a page can have to be included when rendering 612 * 613 * @param int $maxDepth Default is null, which sets no maximum depth. 614 * @return AbstractHelper 615 */ 616 public function setMaxDepth($maxDepth = null) 617 { 618 if (null === $maxDepth || is_int($maxDepth)) { 619 $this->maxDepth = $maxDepth; 620 } else { 621 $this->maxDepth = (int) $maxDepth; 622 } 623 624 return $this; 625 } 626 627 /** 628 * Returns maximum depth a page can have to be included when rendering 629 * 630 * @return int|null 631 */ 632 public function getMaxDepth() 633 { 634 return $this->maxDepth; 635 } 636 637 /** 638 * Sets the minimum depth a page must have to be included when rendering 639 * 640 * @param int $minDepth Default is null, which sets no minimum depth. 641 * @return AbstractHelper 642 */ 643 public function setMinDepth($minDepth = null) 644 { 645 if (null === $minDepth || is_int($minDepth)) { 646 $this->minDepth = $minDepth; 647 } else { 648 $this->minDepth = (int) $minDepth; 649 } 650 651 return $this; 652 } 653 654 /** 655 * Returns minimum depth a page must have to be included when rendering 656 * 657 * @return int|null 658 */ 659 public function getMinDepth() 660 { 661 if (!is_int($this->minDepth) || $this->minDepth < 0) { 662 return 0; 663 } 664 665 return $this->minDepth; 666 } 667 668 /** 669 * Render invisible items? 670 * 671 * @param bool $renderInvisible 672 * @return AbstractHelper 673 */ 674 public function setRenderInvisible($renderInvisible = true) 675 { 676 $this->renderInvisible = (bool) $renderInvisible; 677 return $this; 678 } 679 680 /** 681 * Return renderInvisible flag 682 * 683 * @return bool 684 */ 685 public function getRenderInvisible() 686 { 687 return $this->renderInvisible; 688 } 689 690 /** 691 * Sets ACL role(s) to use when iterating pages 692 * 693 * Implements {@link HelperInterface::setRole()}. 694 * 695 * @param mixed $role [optional] role to set. Expects a string, an 696 * instance of type {@link Acl\Role\RoleInterface}, or null. Default 697 * is null, which will set no role. 698 * @return AbstractHelper 699 * @throws Exception\InvalidArgumentException 700 */ 701 public function setRole($role = null) 702 { 703 if (null === $role || is_string($role) || 704 $role instanceof Acl\Role\RoleInterface 705 ) { 706 $this->role = $role; 707 } else { 708 throw new Exception\InvalidArgumentException(sprintf( 709 '$role must be a string, null, or an instance of ' 710 . 'Zend\Permissions\Role\RoleInterface; %s given', 711 (is_object($role) ? get_class($role) : gettype($role)) 712 )); 713 } 714 715 return $this; 716 } 717 718 /** 719 * Returns ACL role to use when iterating pages, or null if it isn't set 720 * using {@link setRole()} or {@link setDefaultRole()} 721 * 722 * Implements {@link HelperInterface::getRole()}. 723 * 724 * @return string|Acl\Role\RoleInterface|null 725 */ 726 public function getRole() 727 { 728 if ($this->role === null && static::$defaultRole !== null) { 729 return static::$defaultRole; 730 } 731 732 return $this->role; 733 } 734 735 /** 736 * Checks if the helper has an ACL role 737 * 738 * Implements {@link HelperInterface::hasRole()}. 739 * 740 * @return bool 741 */ 742 public function hasRole() 743 { 744 if ($this->role instanceof Acl\Role\RoleInterface 745 || is_string($this->role) 746 || static::$defaultRole instanceof Acl\Role\RoleInterface 747 || is_string(static::$defaultRole) 748 ) { 749 return true; 750 } 751 752 return false; 753 } 754 755 /** 756 * Set the service locator. 757 * 758 * @param ServiceLocatorInterface $serviceLocator 759 * @return AbstractHelper 760 */ 761 public function setServiceLocator(ServiceLocatorInterface $serviceLocator) 762 { 763 $this->serviceLocator = $serviceLocator; 764 return $this; 765 } 766 767 /** 768 * Get the service locator. 769 * 770 * @return ServiceLocatorInterface 771 */ 772 public function getServiceLocator() 773 { 774 return $this->serviceLocator; 775 } 776 777 // Translator methods - Good candidate to refactor as a trait with PHP 5.4 778 779 /** 780 * Sets translator to use in helper 781 * 782 * @param Translator $translator [optional] translator. 783 * Default is null, which sets no translator. 784 * @param string $textDomain [optional] text domain 785 * Default is null, which skips setTranslatorTextDomain 786 * @return AbstractHelper 787 */ 788 public function setTranslator(Translator $translator = null, $textDomain = null) 789 { 790 $this->translator = $translator; 791 if (null !== $textDomain) { 792 $this->setTranslatorTextDomain($textDomain); 793 } 794 795 return $this; 796 } 797 798 /** 799 * Returns translator used in helper 800 * 801 * @return Translator|null 802 */ 803 public function getTranslator() 804 { 805 if (! $this->isTranslatorEnabled()) { 806 return; 807 } 808 809 return $this->translator; 810 } 811 812 /** 813 * Checks if the helper has a translator 814 * 815 * @return bool 816 */ 817 public function hasTranslator() 818 { 819 return (bool) $this->getTranslator(); 820 } 821 822 /** 823 * Sets whether translator is enabled and should be used 824 * 825 * @param bool $enabled 826 * @return AbstractHelper 827 */ 828 public function setTranslatorEnabled($enabled = true) 829 { 830 $this->translatorEnabled = (bool) $enabled; 831 return $this; 832 } 833 834 /** 835 * Returns whether translator is enabled and should be used 836 * 837 * @return bool 838 */ 839 public function isTranslatorEnabled() 840 { 841 return $this->translatorEnabled; 842 } 843 844 /** 845 * Set translation text domain 846 * 847 * @param string $textDomain 848 * @return AbstractHelper 849 */ 850 public function setTranslatorTextDomain($textDomain = 'default') 851 { 852 $this->translatorTextDomain = $textDomain; 853 return $this; 854 } 855 856 /** 857 * Return the translation text domain 858 * 859 * @return string 860 */ 861 public function getTranslatorTextDomain() 862 { 863 return $this->translatorTextDomain; 864 } 865 866 /** 867 * Sets whether ACL should be used 868 * 869 * Implements {@link HelperInterface::setUseAcl()}. 870 * 871 * @param bool $useAcl 872 * @return AbstractHelper 873 */ 874 public function setUseAcl($useAcl = true) 875 { 876 $this->useAcl = (bool) $useAcl; 877 return $this; 878 } 879 880 /** 881 * Returns whether ACL should be used 882 * 883 * Implements {@link HelperInterface::getUseAcl()}. 884 * 885 * @return bool 886 */ 887 public function getUseAcl() 888 { 889 return $this->useAcl; 890 } 891 892 // Static methods: 893 894 /** 895 * Sets default ACL to use if another ACL is not explicitly set 896 * 897 * @param Acl\AclInterface $acl [optional] ACL object. Default is null, which 898 * sets no ACL object. 899 * @return void 900 */ 901 public static function setDefaultAcl(Acl\AclInterface $acl = null) 902 { 903 static::$defaultAcl = $acl; 904 } 905 906 /** 907 * Sets default ACL role(s) to use when iterating pages if not explicitly 908 * set later with {@link setRole()} 909 * 910 * @param mixed $role [optional] role to set. Expects null, string, or an 911 * instance of {@link Acl\Role\RoleInterface}. Default is null, which 912 * sets no default role. 913 * @return void 914 * @throws Exception\InvalidArgumentException if role is invalid 915 */ 916 public static function setDefaultRole($role = null) 917 { 918 if (null === $role 919 || is_string($role) 920 || $role instanceof Acl\Role\RoleInterface 921 ) { 922 static::$defaultRole = $role; 923 } else { 924 throw new Exception\InvalidArgumentException(sprintf( 925 '$role must be null|string|Zend\Permissions\Role\RoleInterface; received "%s"', 926 (is_object($role) ? get_class($role) : gettype($role)) 927 )); 928 } 929 } 930 931 /** 932 * Attaches default ACL listeners, if ACLs are in use 933 */ 934 protected function setDefaultListeners() 935 { 936 if (!$this->getUseAcl()) { 937 return; 938 } 939 940 $this->getEventManager()->getSharedManager()->attach( 941 'Zend\View\Helper\Navigation\AbstractHelper', 942 'isAllowed', 943 array('Zend\View\Helper\Navigation\Listener\AclListener', 'accept') 944 ); 945 } 946} 947