elements * * @category Zend * @package Zend_View * @subpackage Helper * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ class Zend_View_Helper_Navigation_Links extends Zend_View_Helper_Navigation_HelperAbstract { /**#@+ * Constants used for specifying which link types to find and render * * @var int */ const RENDER_ALTERNATE = 0x0001; const RENDER_STYLESHEET = 0x0002; const RENDER_START = 0x0004; const RENDER_NEXT = 0x0008; const RENDER_PREV = 0x0010; const RENDER_CONTENTS = 0x0020; const RENDER_INDEX = 0x0040; const RENDER_GLOSSARY = 0x0080; const RENDER_COPYRIGHT = 0x0100; const RENDER_CHAPTER = 0x0200; const RENDER_SECTION = 0x0400; const RENDER_SUBSECTION = 0x0800; const RENDER_APPENDIX = 0x1000; const RENDER_HELP = 0x2000; const RENDER_BOOKMARK = 0x4000; const RENDER_CUSTOM = 0x8000; const RENDER_ALL = 0xffff; /**#@+**/ /** * Maps render constants to W3C link types * * @var array */ protected static $_RELATIONS = array( self::RENDER_ALTERNATE => 'alternate', self::RENDER_STYLESHEET => 'stylesheet', self::RENDER_START => 'start', self::RENDER_NEXT => 'next', self::RENDER_PREV => 'prev', self::RENDER_CONTENTS => 'contents', self::RENDER_INDEX => 'index', self::RENDER_GLOSSARY => 'glossary', self::RENDER_COPYRIGHT => 'copyright', self::RENDER_CHAPTER => 'chapter', self::RENDER_SECTION => 'section', self::RENDER_SUBSECTION => 'subsection', self::RENDER_APPENDIX => 'appendix', self::RENDER_HELP => 'help', self::RENDER_BOOKMARK => 'bookmark' ); /** * The helper's render flag * * @see render() * @see setRenderFlag() * @var int */ protected $_renderFlag = self::RENDER_ALL; /** * Root container * * Used for preventing methods to traverse above the container given to * the {@link render()} method. * * @see _findRoot() * * @var Zend_Navigation_Container */ protected $_root; /** * View helper entry point: * Retrieves helper and optionally sets container to operate on * * @param Zend_Navigation_Container $container [optional] container to * operate on * @return Zend_View_Helper_Navigation_Links fluent interface, returns * self */ public function links(Zend_Navigation_Container $container = null) { if (null !== $container) { $this->setContainer($container); } return $this; } /** * Magic overload: Proxy calls to {@link findRelation()} or container * * Examples of finder calls: * * // METHOD // SAME AS * $h->findRelNext($page); // $h->findRelation($page, 'rel', 'next') * $h->findRevSection($page); // $h->findRelation($page, 'rev', 'section'); * $h->findRelFoo($page); // $h->findRelation($page, 'rel', 'foo'); * * * @param string $method method name * @param array $arguments method arguments * @throws Zend_Navigation_Exception if method does not exist in container */ public function __call($method, array $arguments = array()) { if (@preg_match('/find(Rel|Rev)(.+)/', $method, $match)) { return $this->findRelation($arguments[0], strtolower($match[1]), strtolower($match[2])); } return parent::__call($method, $arguments); } // Accessors: /** * Sets the helper's render flag * * The helper uses the bitwise '&' operator against the hex values of the * render constants. This means that the flag can is "bitwised" value of * the render constants. Examples: * * // render all links except glossary * $flag = Zend_View_Helper_Navigation_Links:RENDER_ALL ^ * Zend_View_Helper_Navigation_Links:RENDER_GLOSSARY; * $helper->setRenderFlag($flag); * * // render only chapters and sections * $flag = Zend_View_Helper_Navigation_Links:RENDER_CHAPTER | * Zend_View_Helper_Navigation_Links:RENDER_SECTION; * $helper->setRenderFlag($flag); * * // render only relations that are not native W3C relations * $helper->setRenderFlag(Zend_View_Helper_Navigation_Links:RENDER_CUSTOM); * * // render all relations (default) * $helper->setRenderFlag(Zend_View_Helper_Navigation_Links:RENDER_ALL); * * * Note that custom relations can also be rendered directly using the * {@link renderLink()} method. * * @param int $renderFlag render flag * @return Zend_View_Helper_Navigation_Links fluent interface, returns self */ public function setRenderFlag($renderFlag) { $this->_renderFlag = (int) $renderFlag; return $this; } /** * Returns the helper's render flag * * @return int render flag */ public function getRenderFlag() { return $this->_renderFlag; } // Finder methods: /** * Finds all relations (forward and reverse) for the given $page * * The form of the returned array: * * // $page denotes an instance of Zend_Navigation_Page * $returned = array( * 'rel' => array( * 'alternate' => array($page, $page, $page), * 'start' => array($page), * 'next' => array($page), * 'prev' => array($page), * 'canonical' => array($page) * ), * 'rev' => array( * 'section' => array($page) * ) * ); * * * @param Zend_Navigation_Page $page page to find links for * @return array related pages */ public function findAllRelations(Zend_Navigation_Page $page, $flag = null) { if (!is_int($flag)) { $flag = self::RENDER_ALL; } $result = array('rel' => array(), 'rev' => array()); $native = array_values(self::$_RELATIONS); foreach (array_keys($result) as $rel) { $meth = 'getDefined' . ucfirst($rel); $types = array_merge($native, array_diff($page->$meth(), $native)); foreach ($types as $type) { if (!$relFlag = array_search($type, self::$_RELATIONS)) { $relFlag = self::RENDER_CUSTOM; } if (!($flag & $relFlag)) { continue; } if ($found = $this->findRelation($page, $rel, $type)) { if (!is_array($found)) { $found = array($found); } $result[$rel][$type] = $found; } } } return $result; } /** * Finds relations of the given $rel=$type from $page * * This method will first look for relations in the page instance, then * by searching the root container if nothing was found in the page. * * @param Zend_Navigation_Page $page page to find relations for * @param string $rel relation, "rel" or "rev" * @param string $type link type, e.g. 'start', 'next' * @return Zend_Navigaiton_Page|array|null page(s), or null if not found * @throws Zend_View_Exception if $rel is not "rel" or "rev" */ public function findRelation(Zend_Navigation_Page $page, $rel, $type) { if (!in_array($rel, array('rel', 'rev'))) { require_once 'Zend/View/Exception.php'; $e = new Zend_View_Exception(sprintf( 'Invalid argument: $rel must be "rel" or "rev"; "%s" given', $rel)); $e->setView($this->view); throw $e; } if (!$result = $this->_findFromProperty($page, $rel, $type)) { $result = $this->_findFromSearch($page, $rel, $type); } return $result; } /** * Finds relations of given $type for $page by checking if the * relation is specified as a property of $page * * @param Zend_Navigation_Page $page page to find relations for * @param string $rel relation, 'rel' or 'rev' * @param string $type link type, e.g. 'start', 'next' * @return Zend_Navigation_Page|array|null page(s), or null if not found */ protected function _findFromProperty(Zend_Navigation_Page $page, $rel, $type) { $method = 'get' . ucfirst($rel); if ($result = $page->$method($type)) { if ($result = $this->_convertToPages($result)) { if (!is_array($result)) { $result = array($result); } foreach ($result as $key => $page) { if (!$this->accept($page)) { unset($result[$key]); } } return count($result) == 1 ? $result[0] : $result; } } return null; } /** * Finds relations of given $rel=$type for $page by using the helper to * search for the relation in the root container * * @param Zend_Navigation_Page $page page to find relations for * @param string $rel relation, 'rel' or 'rev' * @param string $type link type, e.g. 'start', 'next', etc * @return array|null array of pages, or null if not found */ protected function _findFromSearch(Zend_Navigation_Page $page, $rel, $type) { $found = null; $method = 'search' . ucfirst($rel) . ucfirst($type); if (method_exists($this, $method)) { $found = $this->$method($page); } return $found; } // Search methods: /** * Searches the root container for the forward 'start' relation of the given * $page * * From {@link http://www.w3.org/TR/html4/types.html#type-links}: * Refers to the first document in a collection of documents. This link type * tells search engines which document is considered by the author to be the * starting point of the collection. * * @param Zend_Navigation_Page $page page to find relation for * @return Zend_Navigation_Page|null page or null */ public function searchRelStart(Zend_Navigation_Page $page) { $found = $this->_findRoot($page); if (!$found instanceof Zend_Navigation_Page) { $found->rewind(); $found = $found->current(); } if ($found === $page || !$this->accept($found)) { $found = null; } return $found; } /** * Searches the root container for the forward 'next' relation of the given * $page * * From {@link http://www.w3.org/TR/html4/types.html#type-links}: * Refers to the next document in a linear sequence of documents. User * agents may choose to preload the "next" document, to reduce the perceived * load time. * * @param Zend_Navigation_Page $page page to find relation for * @return Zend_Navigation_Page|null page(s) or null */ public function searchRelNext(Zend_Navigation_Page $page) { $found = null; $break = false; $iterator = new RecursiveIteratorIterator($this->_findRoot($page), RecursiveIteratorIterator::SELF_FIRST); foreach ($iterator as $intermediate) { if ($intermediate === $page) { // current page; break at next accepted page $break = true; continue; } if ($break && $this->accept($intermediate)) { $found = $intermediate; break; } } return $found; } /** * Searches the root container for the forward 'prev' relation of the given * $page * * From {@link http://www.w3.org/TR/html4/types.html#type-links}: * Refers to the previous document in an ordered series of documents. Some * user agents also support the synonym "Previous". * * @param Zend_Navigation_Page $page page to find relation for * @return Zend_Navigation_Page|null page or null */ public function searchRelPrev(Zend_Navigation_Page $page) { $found = null; $prev = null; $iterator = new RecursiveIteratorIterator( $this->_findRoot($page), RecursiveIteratorIterator::SELF_FIRST); foreach ($iterator as $intermediate) { if (!$this->accept($intermediate)) { continue; } if ($intermediate === $page) { $found = $prev; break; } $prev = $intermediate; } return $found; } /** * Searches the root container for forward 'chapter' relations of the given * $page * * From {@link http://www.w3.org/TR/html4/types.html#type-links}: * Refers to a document serving as a chapter in a collection of documents. * * @param Zend_Navigation_Page $page page to find relation for * @return Zend_Navigation_Page|array|null page(s) or null */ public function searchRelChapter(Zend_Navigation_Page $page) { $found = array(); // find first level of pages $root = $this->_findRoot($page); // find start page(s) $start = $this->findRelation($page, 'rel', 'start'); if (!is_array($start)) { $start = array($start); } foreach ($root as $chapter) { // exclude self and start page from chapters if ($chapter !== $page && !in_array($chapter, $start) && $this->accept($chapter)) { $found[] = $chapter; } } switch (count($found)) { case 0: return null; case 1: return $found[0]; default: return $found; } } /** * Searches the root container for forward 'section' relations of the given * $page * * From {@link http://www.w3.org/TR/html4/types.html#type-links}: * Refers to a document serving as a section in a collection of documents. * * @param Zend_Navigation_Page $page page to find relation for * @return Zend_Navigation_Page|array|null page(s) or null */ public function searchRelSection(Zend_Navigation_Page $page) { $found = array(); // check if given page has pages and is a chapter page if ($page->hasPages() && $this->_findRoot($page)->hasPage($page)) { foreach ($page as $section) { if ($this->accept($section)) { $found[] = $section; } } } switch (count($found)) { case 0: return null; case 1: return $found[0]; default: return $found; } } /** * Searches the root container for forward 'subsection' relations of the * given $page * * From {@link http://www.w3.org/TR/html4/types.html#type-links}: * Refers to a document serving as a subsection in a collection of * documents. * * @param Zend_Navigation_Page $page page to find relation for * @return Zend_Navigation_Page|array|null page(s) or null */ public function searchRelSubsection(Zend_Navigation_Page $page) { $found = array(); if ($page->hasPages()) { // given page has child pages, loop chapters foreach ($this->_findRoot($page) as $chapter) { // is page a section? if ($chapter->hasPage($page)) { foreach ($page as $subsection) { if ($this->accept($subsection)) { $found[] = $subsection; } } } } } switch (count($found)) { case 0: return null; case 1: return $found[0]; default: return $found; } } /** * Searches the root container for the reverse 'section' relation of the * given $page * * From {@link http://www.w3.org/TR/html4/types.html#type-links}: * Refers to a document serving as a section in a collection of documents. * * @param Zend_Navigation_Page $page page to find relation for * @return Zend_Navigation_Page|null page(s) or null */ public function searchRevSection(Zend_Navigation_Page $page) { $found = null; if ($parent = $page->getParent()) { if ($parent instanceof Zend_Navigation_Page && $this->_findRoot($page)->hasPage($parent)) { $found = $parent; } } return $found; } /** * Searches the root container for the reverse 'section' relation of the * given $page * * From {@link http://www.w3.org/TR/html4/types.html#type-links}: * Refers to a document serving as a subsection in a collection of * documents. * * @param Zend_Navigation_Page $page page to find relation for * @return Zend_Navigation_Page|null page(s) or null */ public function searchRevSubsection(Zend_Navigation_Page $page) { $found = null; if ($parent = $page->getParent()) { if ($parent instanceof Zend_Navigation_Page) { $root = $this->_findRoot($page); foreach ($root as $chapter) { if ($chapter->hasPage($parent)) { $found = $parent; break; } } } } return $found; } // Util methods: /** * Returns the root container of the given page * * When rendering a container, the render method still store the given * container as the root container, and unset it when done rendering. This * makes sure finder methods will not traverse above the container given * to the render method. * * @param Zend_Navigaiton_Page $page page to find root for * @return Zend_Navigation_Container the root container of the given page */ protected function _findRoot(Zend_Navigation_Page $page) { if ($this->_root) { return $this->_root; } $root = $page; while ($parent = $page->getParent()) { $root = $parent; if ($parent instanceof Zend_Navigation_Page) { $page = $parent; } else { break; } } return $root; } /** * Converts a $mixed value to an array of pages * * @param mixed $mixed mixed value to get page(s) from * @param bool $recursive whether $value should be looped * if it is an array or a config * @return Zend_Navigation_Page|array|null empty if unable to convert */ protected function _convertToPages($mixed, $recursive = true) { if (is_object($mixed)) { if ($mixed instanceof Zend_Navigation_Page) { // value is a page instance; return directly return $mixed; } elseif ($mixed instanceof Zend_Navigation_Container) { // value is a container; return pages in it $pages = array(); foreach ($mixed as $page) { $pages[] = $page; } return $pages; } elseif ($mixed instanceof Zend_Config) { // convert config object to array and extract return $this->_convertToPages($mixed->toArray(), $recursive); } } elseif (is_string($mixed)) { // value is a string; make an URI page return Zend_Navigation_Page::factory(array( 'type' => 'uri', 'uri' => $mixed )); } elseif (is_array($mixed) && !empty($mixed)) { if ($recursive && is_numeric(key($mixed))) { // first key is numeric; assume several pages $pages = array(); foreach ($mixed as $value) { if ($value = $this->_convertToPages($value, false)) { $pages[] = $value; } } return $pages; } else { // pass array to factory directly try { $page = Zend_Navigation_Page::factory($mixed); return $page; } catch (Exception $e) { } } } // nothing found return null; } // Render methods: /** * Renders the given $page as a link element, with $attrib = $relation * * @param Zend_Navigation_Page $page the page to render the link for * @param string $attrib the attribute to use for $type, * either 'rel' or 'rev' * @param string $relation relation type, muse be one of; * alternate, appendix, bookmark, * chapter, contents, copyright, * glossary, help, home, index, next, * prev, section, start, stylesheet, * subsection * @return string rendered link element * @throws Zend_View_Exception if $attrib is invalid */ public function renderLink(Zend_Navigation_Page $page, $attrib, $relation) { if (!in_array($attrib, array('rel', 'rev'))) { require_once 'Zend/View/Exception.php'; $e = new Zend_View_Exception(sprintf( 'Invalid relation attribute "%s", must be "rel" or "rev"', $attrib)); $e->setView($this->view); throw $e; } if (!$href = $page->getHref()) { return ''; } // TODO: add more attribs // http://www.w3.org/TR/html401/struct/links.html#h-12.2 $attribs = array( $attrib => $relation, 'href' => $href, 'title' => $page->getLabel() ); return '_htmlAttribs($attribs) . $this->getClosingBracket(); } // Zend_View_Helper_Navigation_Helper: /** * Renders helper * * Implements {@link Zend_View_Helper_Navigation_Helper::render()}. * * @param Zend_Navigation_Container $container [optional] container to * render. Default is to * render the container * registered in the helper. * @return string helper output */ public function render(Zend_Navigation_Container $container = null) { if (null === $container) { $container = $this->getContainer(); } if ($active = $this->findActive($container)) { $active = $active['page']; } else { // no active page return ''; } $output = ''; $indent = $this->getIndent(); $this->_root = $container; $result = $this->findAllRelations($active, $this->getRenderFlag()); foreach ($result as $attrib => $types) { foreach ($types as $relation => $pages) { foreach ($pages as $page) { if ($r = $this->renderLink($page, $attrib, $relation)) { $output .= $indent . $r . self::EOL; } } } } $this->_root = null; // return output (trim last newline by spec) return strlen($output) ? rtrim($output, self::EOL) : ''; } }