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 Traversable; 14use Zend\Navigation\AbstractContainer; 15use Zend\Navigation\Page\AbstractPage; 16use Zend\Stdlib\ArrayUtils; 17use Zend\Stdlib\ErrorHandler; 18use Zend\View\Exception; 19 20/** 21 * Helper for printing <link> elements 22 */ 23class Links extends AbstractHelper 24{ 25 /** 26 * Constants used for specifying which link types to find and render 27 * 28 * @var int 29 */ 30 const RENDER_ALTERNATE = 0x0001; 31 const RENDER_STYLESHEET = 0x0002; 32 const RENDER_START = 0x0004; 33 const RENDER_NEXT = 0x0008; 34 const RENDER_PREV = 0x0010; 35 const RENDER_CONTENTS = 0x0020; 36 const RENDER_INDEX = 0x0040; 37 const RENDER_GLOSSARY = 0x0080; 38 const RENDER_COPYRIGHT = 0x0100; 39 const RENDER_CHAPTER = 0x0200; 40 const RENDER_SECTION = 0x0400; 41 const RENDER_SUBSECTION = 0x0800; 42 const RENDER_APPENDIX = 0x1000; 43 const RENDER_HELP = 0x2000; 44 const RENDER_BOOKMARK = 0x4000; 45 const RENDER_CUSTOM = 0x8000; 46 const RENDER_ALL = 0xffff; 47 48 /** 49 * Maps render constants to W3C link types 50 * 51 * @var array 52 */ 53 protected static $RELATIONS = array( 54 self::RENDER_ALTERNATE => 'alternate', 55 self::RENDER_STYLESHEET => 'stylesheet', 56 self::RENDER_START => 'start', 57 self::RENDER_NEXT => 'next', 58 self::RENDER_PREV => 'prev', 59 self::RENDER_CONTENTS => 'contents', 60 self::RENDER_INDEX => 'index', 61 self::RENDER_GLOSSARY => 'glossary', 62 self::RENDER_COPYRIGHT => 'copyright', 63 self::RENDER_CHAPTER => 'chapter', 64 self::RENDER_SECTION => 'section', 65 self::RENDER_SUBSECTION => 'subsection', 66 self::RENDER_APPENDIX => 'appendix', 67 self::RENDER_HELP => 'help', 68 self::RENDER_BOOKMARK => 'bookmark', 69 ); 70 71 /** 72 * The helper's render flag 73 * 74 * @see render() 75 * @see setRenderFlag() 76 * @var int 77 */ 78 protected $renderFlag = self::RENDER_ALL; 79 80 /** 81 * Root container 82 * 83 * Used for preventing methods to traverse above the container given to 84 * the {@link render()} method. 85 * 86 * @see _findRoot() 87 * @var AbstractContainer 88 */ 89 protected $root; 90 91 /** 92 * Helper entry point 93 * 94 * @param string|AbstractContainer $container container to operate on 95 * @return Links 96 */ 97 public function __invoke($container = null) 98 { 99 if (null !== $container) { 100 $this->setContainer($container); 101 } 102 103 return $this; 104 } 105 106 /** 107 * Magic overload: Proxy calls to {@link findRelation()} or container 108 * 109 * Examples of finder calls: 110 * <code> 111 * // METHOD // SAME AS 112 * $h->findRelNext($page); // $h->findRelation($page, 'rel', 'next') 113 * $h->findRevSection($page); // $h->findRelation($page, 'rev', 'section'); 114 * $h->findRelFoo($page); // $h->findRelation($page, 'rel', 'foo'); 115 * </code> 116 * 117 * @param string $method 118 * @param array $arguments 119 * @return mixed 120 * @throws Exception\ExceptionInterface 121 */ 122 public function __call($method, array $arguments = array()) 123 { 124 ErrorHandler::start(E_WARNING); 125 $result = preg_match('/find(Rel|Rev)(.+)/', $method, $match); 126 ErrorHandler::stop(); 127 if ($result) { 128 return $this->findRelation($arguments[0], strtolower($match[1]), strtolower($match[2])); 129 } 130 131 return parent::__call($method, $arguments); 132 } 133 134 /** 135 * Renders helper 136 * 137 * Implements {@link HelperInterface::render()}. 138 * 139 * @param AbstractContainer|string|null $container [optional] container to render. 140 * Default is to render the 141 * container registered in the 142 * helper. 143 * @return string 144 */ 145 public function render($container = null) 146 { 147 $this->parseContainer($container); 148 if (null === $container) { 149 $container = $this->getContainer(); 150 } 151 152 $active = $this->findActive($container); 153 if ($active) { 154 $active = $active['page']; 155 } else { 156 // no active page 157 return ''; 158 } 159 160 $output = ''; 161 $indent = $this->getIndent(); 162 $this->root = $container; 163 164 $result = $this->findAllRelations($active, $this->getRenderFlag()); 165 foreach ($result as $attrib => $types) { 166 foreach ($types as $relation => $pages) { 167 foreach ($pages as $page) { 168 $r = $this->renderLink($page, $attrib, $relation); 169 if ($r) { 170 $output .= $indent . $r . PHP_EOL; 171 } 172 } 173 } 174 } 175 176 $this->root = null; 177 178 // return output (trim last newline by spec) 179 return strlen($output) ? rtrim($output, PHP_EOL) : ''; 180 } 181 182 /** 183 * Renders the given $page as a link element, with $attrib = $relation 184 * 185 * @param AbstractPage $page the page to render the link for 186 * @param string $attrib the attribute to use for $type, 187 * either 'rel' or 'rev' 188 * @param string $relation relation type, muse be one of; 189 * alternate, appendix, bookmark, 190 * chapter, contents, copyright, 191 * glossary, help, home, index, next, 192 * prev, section, start, stylesheet, 193 * subsection 194 * @return string 195 * @throws Exception\DomainException 196 */ 197 public function renderLink(AbstractPage $page, $attrib, $relation) 198 { 199 if (!in_array($attrib, array('rel', 'rev'))) { 200 throw new Exception\DomainException(sprintf( 201 'Invalid relation attribute "%s", must be "rel" or "rev"', 202 $attrib 203 )); 204 } 205 206 if (!$href = $page->getHref()) { 207 return ''; 208 } 209 210 // TODO: add more attribs 211 // http://www.w3.org/TR/html401/struct/links.html#h-12.2 212 $attribs = array( 213 $attrib => $relation, 214 'href' => $href, 215 'title' => $page->getLabel() 216 ); 217 218 return '<link' . 219 $this->htmlAttribs($attribs) . 220 $this->getClosingBracket(); 221 } 222 223 // Finder methods: 224 225 /** 226 * Finds all relations (forward and reverse) for the given $page 227 * 228 * The form of the returned array: 229 * <code> 230 * // $page denotes an instance of Zend\Navigation\Page\AbstractPage 231 * $returned = array( 232 * 'rel' => array( 233 * 'alternate' => array($page, $page, $page), 234 * 'start' => array($page), 235 * 'next' => array($page), 236 * 'prev' => array($page), 237 * 'canonical' => array($page) 238 * ), 239 * 'rev' => array( 240 * 'section' => array($page) 241 * ) 242 * ); 243 * </code> 244 * 245 * @param AbstractPage $page page to find links for 246 * @param null|int 247 * @return array 248 */ 249 public function findAllRelations(AbstractPage $page, $flag = null) 250 { 251 if (!is_int($flag)) { 252 $flag = self::RENDER_ALL; 253 } 254 255 $result = array('rel' => array(), 'rev' => array()); 256 $native = array_values(static::$RELATIONS); 257 258 foreach (array_keys($result) as $rel) { 259 $meth = 'getDefined' . ucfirst($rel); 260 $types = array_merge($native, array_diff($page->$meth(), $native)); 261 262 foreach ($types as $type) { 263 if (!$relFlag = array_search($type, static::$RELATIONS)) { 264 $relFlag = self::RENDER_CUSTOM; 265 } 266 if (!($flag & $relFlag)) { 267 continue; 268 } 269 270 $found = $this->findRelation($page, $rel, $type); 271 if ($found) { 272 if (!is_array($found)) { 273 $found = array($found); 274 } 275 $result[$rel][$type] = $found; 276 } 277 } 278 } 279 280 return $result; 281 } 282 283 /** 284 * Finds relations of the given $rel=$type from $page 285 * 286 * This method will first look for relations in the page instance, then 287 * by searching the root container if nothing was found in the page. 288 * 289 * @param AbstractPage $page page to find relations for 290 * @param string $rel relation, "rel" or "rev" 291 * @param string $type link type, e.g. 'start', 'next' 292 * @return AbstractPage|array|null 293 * @throws Exception\DomainException if $rel is not "rel" or "rev" 294 */ 295 public function findRelation(AbstractPage $page, $rel, $type) 296 { 297 if (!in_array($rel, array('rel', 'rev'))) { 298 throw new Exception\DomainException(sprintf( 299 'Invalid argument: $rel must be "rel" or "rev"; "%s" given', 300 $rel 301 )); 302 } 303 304 if (!$result = $this->findFromProperty($page, $rel, $type)) { 305 $result = $this->findFromSearch($page, $rel, $type); 306 } 307 308 return $result; 309 } 310 311 /** 312 * Finds relations of given $type for $page by checking if the 313 * relation is specified as a property of $page 314 * 315 * @param AbstractPage $page page to find relations for 316 * @param string $rel relation, 'rel' or 'rev' 317 * @param string $type link type, e.g. 'start', 'next' 318 * @return AbstractPage|array|null 319 */ 320 protected function findFromProperty(AbstractPage $page, $rel, $type) 321 { 322 $method = 'get' . ucfirst($rel); 323 $result = $page->$method($type); 324 if ($result) { 325 $result = $this->convertToPages($result); 326 if ($result) { 327 if (!is_array($result)) { 328 $result = array($result); 329 } 330 331 foreach ($result as $key => $page) { 332 if (!$this->accept($page)) { 333 unset($result[$key]); 334 } 335 } 336 337 return count($result) == 1 ? $result[0] : $result; 338 } 339 } 340 341 return; 342 } 343 344 /** 345 * Finds relations of given $rel=$type for $page by using the helper to 346 * search for the relation in the root container 347 * 348 * @param AbstractPage $page page to find relations for 349 * @param string $rel relation, 'rel' or 'rev' 350 * @param string $type link type, e.g. 'start', 'next', etc 351 * @return array|null 352 */ 353 protected function findFromSearch(AbstractPage $page, $rel, $type) 354 { 355 $found = null; 356 357 $method = 'search' . ucfirst($rel) . ucfirst($type); 358 if (method_exists($this, $method)) { 359 $found = $this->$method($page); 360 } 361 362 return $found; 363 } 364 365 // Search methods: 366 367 /** 368 * Searches the root container for the forward 'start' relation of the given 369 * $page 370 * 371 * From {@link http://www.w3.org/TR/html4/types.html#type-links}: 372 * Refers to the first document in a collection of documents. This link type 373 * tells search engines which document is considered by the author to be the 374 * starting point of the collection. 375 * 376 * @param AbstractPage $page 377 * @return AbstractPage|null 378 */ 379 public function searchRelStart(AbstractPage $page) 380 { 381 $found = $this->findRoot($page); 382 if (!$found instanceof AbstractPage) { 383 $found->rewind(); 384 $found = $found->current(); 385 } 386 387 if ($found === $page || !$this->accept($found)) { 388 $found = null; 389 } 390 391 return $found; 392 } 393 394 /** 395 * Searches the root container for the forward 'next' relation of the given 396 * $page 397 * 398 * From {@link http://www.w3.org/TR/html4/types.html#type-links}: 399 * Refers to the next document in a linear sequence of documents. User 400 * agents may choose to preload the "next" document, to reduce the perceived 401 * load time. 402 * 403 * @param AbstractPage $page 404 * @return AbstractPage|null 405 */ 406 public function searchRelNext(AbstractPage $page) 407 { 408 $found = null; 409 $break = false; 410 $iterator = new RecursiveIteratorIterator($this->findRoot($page), RecursiveIteratorIterator::SELF_FIRST); 411 foreach ($iterator as $intermediate) { 412 if ($intermediate === $page) { 413 // current page; break at next accepted page 414 $break = true; 415 continue; 416 } 417 418 if ($break && $this->accept($intermediate)) { 419 $found = $intermediate; 420 break; 421 } 422 } 423 424 return $found; 425 } 426 427 /** 428 * Searches the root container for the forward 'prev' relation of the given 429 * $page 430 * 431 * From {@link http://www.w3.org/TR/html4/types.html#type-links}: 432 * Refers to the previous document in an ordered series of documents. Some 433 * user agents also support the synonym "Previous". 434 * 435 * @param AbstractPage $page 436 * @return AbstractPage|null 437 */ 438 public function searchRelPrev(AbstractPage $page) 439 { 440 $found = null; 441 $prev = null; 442 $iterator = new RecursiveIteratorIterator( 443 $this->findRoot($page), 444 RecursiveIteratorIterator::SELF_FIRST 445 ); 446 foreach ($iterator as $intermediate) { 447 if (!$this->accept($intermediate)) { 448 continue; 449 } 450 if ($intermediate === $page) { 451 $found = $prev; 452 break; 453 } 454 455 $prev = $intermediate; 456 } 457 458 return $found; 459 } 460 461 /** 462 * Searches the root container for forward 'chapter' relations of the given 463 * $page 464 * 465 * From {@link http://www.w3.org/TR/html4/types.html#type-links}: 466 * Refers to a document serving as a chapter in a collection of documents. 467 * 468 * @param AbstractPage $page 469 * @return AbstractPage|array|null 470 */ 471 public function searchRelChapter(AbstractPage $page) 472 { 473 $found = array(); 474 475 // find first level of pages 476 $root = $this->findRoot($page); 477 478 // find start page(s) 479 $start = $this->findRelation($page, 'rel', 'start'); 480 if (!is_array($start)) { 481 $start = array($start); 482 } 483 484 foreach ($root as $chapter) { 485 // exclude self and start page from chapters 486 if ($chapter !== $page && 487 !in_array($chapter, $start) && 488 $this->accept($chapter)) { 489 $found[] = $chapter; 490 } 491 } 492 493 switch (count($found)) { 494 case 0: 495 return; 496 case 1: 497 return $found[0]; 498 default: 499 return $found; 500 } 501 } 502 503 /** 504 * Searches the root container for forward 'section' relations of the given 505 * $page 506 * 507 * From {@link http://www.w3.org/TR/html4/types.html#type-links}: 508 * Refers to a document serving as a section in a collection of documents. 509 * 510 * @param AbstractPage $page 511 * @return AbstractPage|array|null 512 */ 513 public function searchRelSection(AbstractPage $page) 514 { 515 $found = array(); 516 517 // check if given page has pages and is a chapter page 518 if ($page->hasPages() && $this->findRoot($page)->hasPage($page)) { 519 foreach ($page as $section) { 520 if ($this->accept($section)) { 521 $found[] = $section; 522 } 523 } 524 } 525 526 switch (count($found)) { 527 case 0: 528 return; 529 case 1: 530 return $found[0]; 531 default: 532 return $found; 533 } 534 } 535 536 /** 537 * Searches the root container for forward 'subsection' relations of the 538 * given $page 539 * 540 * From {@link http://www.w3.org/TR/html4/types.html#type-links}: 541 * Refers to a document serving as a subsection in a collection of 542 * documents. 543 * 544 * @param AbstractPage $page 545 * @return AbstractPage|array|null 546 */ 547 public function searchRelSubsection(AbstractPage $page) 548 { 549 $found = array(); 550 551 if ($page->hasPages()) { 552 // given page has child pages, loop chapters 553 foreach ($this->findRoot($page) as $chapter) { 554 // is page a section? 555 if ($chapter->hasPage($page)) { 556 foreach ($page as $subsection) { 557 if ($this->accept($subsection)) { 558 $found[] = $subsection; 559 } 560 } 561 } 562 } 563 } 564 565 switch (count($found)) { 566 case 0: 567 return; 568 case 1: 569 return $found[0]; 570 default: 571 return $found; 572 } 573 } 574 575 /** 576 * Searches the root container for the reverse 'section' relation of the 577 * given $page 578 * 579 * From {@link http://www.w3.org/TR/html4/types.html#type-links}: 580 * Refers to a document serving as a section in a collection of documents. 581 * 582 * @param AbstractPage $page 583 * @return AbstractPage|null 584 */ 585 public function searchRevSection(AbstractPage $page) 586 { 587 $found = null; 588 $parent = $page->getParent(); 589 if ($parent) { 590 if ($parent instanceof AbstractPage && 591 $this->findRoot($page)->hasPage($parent)) { 592 $found = $parent; 593 } 594 } 595 596 return $found; 597 } 598 599 /** 600 * Searches the root container for the reverse 'section' relation of the 601 * given $page 602 * 603 * From {@link http://www.w3.org/TR/html4/types.html#type-links}: 604 * Refers to a document serving as a subsection in a collection of 605 * documents. 606 * 607 * @param AbstractPage $page 608 * @return AbstractPage|null 609 */ 610 public function searchRevSubsection(AbstractPage $page) 611 { 612 $found = null; 613 $parent = $page->getParent(); 614 if ($parent) { 615 if ($parent instanceof AbstractPage) { 616 $root = $this->findRoot($page); 617 foreach ($root as $chapter) { 618 if ($chapter->hasPage($parent)) { 619 $found = $parent; 620 break; 621 } 622 } 623 } 624 } 625 626 return $found; 627 } 628 629 // Util methods: 630 631 /** 632 * Returns the root container of the given page 633 * 634 * When rendering a container, the render method still store the given 635 * container as the root container, and unset it when done rendering. This 636 * makes sure finder methods will not traverse above the container given 637 * to the render method. 638 * 639 * @param AbstractPage $page 640 * @return AbstractContainer 641 */ 642 protected function findRoot(AbstractPage $page) 643 { 644 if ($this->root) { 645 return $this->root; 646 } 647 648 $root = $page; 649 650 while ($parent = $page->getParent()) { 651 $root = $parent; 652 if ($parent instanceof AbstractPage) { 653 $page = $parent; 654 } else { 655 break; 656 } 657 } 658 659 return $root; 660 } 661 662 /** 663 * Converts a $mixed value to an array of pages 664 * 665 * @param mixed $mixed mixed value to get page(s) from 666 * @param bool $recursive whether $value should be looped 667 * if it is an array or a config 668 * @return AbstractPage|array|null 669 */ 670 protected function convertToPages($mixed, $recursive = true) 671 { 672 if ($mixed instanceof AbstractPage) { 673 // value is a page instance; return directly 674 return $mixed; 675 } elseif ($mixed instanceof AbstractContainer) { 676 // value is a container; return pages in it 677 $pages = array(); 678 foreach ($mixed as $page) { 679 $pages[] = $page; 680 } 681 return $pages; 682 } elseif ($mixed instanceof Traversable) { 683 $mixed = ArrayUtils::iteratorToArray($mixed); 684 } elseif (is_string($mixed)) { 685 // value is a string; make a URI page 686 return AbstractPage::factory(array( 687 'type' => 'uri', 688 'uri' => $mixed 689 )); 690 } 691 692 if (is_array($mixed) && !empty($mixed)) { 693 if ($recursive && is_numeric(key($mixed))) { 694 // first key is numeric; assume several pages 695 $pages = array(); 696 foreach ($mixed as $value) { 697 $value = $this->convertToPages($value, false); 698 if ($value) { 699 $pages[] = $value; 700 } 701 } 702 return $pages; 703 } else { 704 // pass array to factory directly 705 try { 706 $page = AbstractPage::factory($mixed); 707 return $page; 708 } catch (\Exception $e) { 709 } 710 } 711 } 712 713 // nothing found 714 return; 715 } 716 717 /** 718 * Sets the helper's render flag 719 * 720 * The helper uses the bitwise '&' operator against the hex values of the 721 * render constants. This means that the flag can is "bitwised" value of 722 * the render constants. Examples: 723 * <code> 724 * // render all links except glossary 725 * $flag = Links:RENDER_ALL ^ Links:RENDER_GLOSSARY; 726 * $helper->setRenderFlag($flag); 727 * 728 * // render only chapters and sections 729 * $flag = Links:RENDER_CHAPTER | Links:RENDER_SECTION; 730 * $helper->setRenderFlag($flag); 731 * 732 * // render only relations that are not native W3C relations 733 * $helper->setRenderFlag(Links:RENDER_CUSTOM); 734 * 735 * // render all relations (default) 736 * $helper->setRenderFlag(Links:RENDER_ALL); 737 * </code> 738 * 739 * Note that custom relations can also be rendered directly using the 740 * {@link renderLink()} method. 741 * 742 * @param int $renderFlag 743 * @return Links 744 */ 745 public function setRenderFlag($renderFlag) 746 { 747 $this->renderFlag = (int) $renderFlag; 748 749 return $this; 750 } 751 752 /** 753 * Returns the helper's render flag 754 * 755 * @return int 756 */ 757 public function getRenderFlag() 758 { 759 return $this->renderFlag; 760 } 761} 762