PageRenderTime 44ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/library/Zend/Navigation/AbstractContainer.php

https://github.com/Thinkscape/zf2
PHP | 511 lines | 244 code | 58 blank | 209 comment | 28 complexity | c79b323bbadcc08b97f1537e1b4fb896 MD5 | raw file
  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-2014 Zend Technologies USA Inc. (http://www.zend.com)
  7. * @license http://framework.zend.com/license/new-bsd New BSD License
  8. */
  9. namespace Zend\Navigation;
  10. use Countable;
  11. use RecursiveIterator;
  12. use RecursiveIteratorIterator;
  13. use Traversable;
  14. use Zend\Stdlib\ErrorHandler;
  15. /**
  16. * Zend\Navigation\Container
  17. *
  18. * AbstractContainer class for Zend\Navigation\Page classes.
  19. */
  20. abstract class AbstractContainer implements Countable, RecursiveIterator
  21. {
  22. /**
  23. * Contains sub pages
  24. *
  25. * @var array
  26. */
  27. protected $pages = array();
  28. /**
  29. * An index that contains the order in which to iterate pages
  30. *
  31. * @var array
  32. */
  33. protected $index = array();
  34. /**
  35. * Whether index is dirty and needs to be re-arranged
  36. *
  37. * @var bool
  38. */
  39. protected $dirtyIndex = false;
  40. // Internal methods:
  41. /**
  42. * Sorts the page index according to page order
  43. *
  44. * @return void
  45. */
  46. protected function sort()
  47. {
  48. if (!$this->dirtyIndex) {
  49. return;
  50. }
  51. $newIndex = array();
  52. $index = 0;
  53. foreach ($this->pages as $hash => $page) {
  54. $order = $page->getOrder();
  55. if ($order === null) {
  56. $newIndex[$hash] = $index;
  57. $index++;
  58. } else {
  59. $newIndex[$hash] = $order;
  60. }
  61. }
  62. asort($newIndex);
  63. $this->index = $newIndex;
  64. $this->dirtyIndex = false;
  65. }
  66. // Public methods:
  67. /**
  68. * Notifies container that the order of pages are updated
  69. *
  70. * @return void
  71. */
  72. public function notifyOrderUpdated()
  73. {
  74. $this->dirtyIndex = true;
  75. }
  76. /**
  77. * Adds a page to the container
  78. *
  79. * This method will inject the container as the given page's parent by
  80. * calling {@link Page\AbstractPage::setParent()}.
  81. *
  82. * @param Page\AbstractPage|array|Traversable $page page to add
  83. * @return self fluent interface, returns self
  84. * @throws Exception\InvalidArgumentException if page is invalid
  85. */
  86. public function addPage($page)
  87. {
  88. if ($page === $this) {
  89. throw new Exception\InvalidArgumentException(
  90. 'A page cannot have itself as a parent'
  91. );
  92. }
  93. if (!$page instanceof Page\AbstractPage) {
  94. if (!is_array($page) && !$page instanceof Traversable) {
  95. throw new Exception\InvalidArgumentException(
  96. 'Invalid argument: $page must be an instance of '
  97. . 'Zend\Navigation\Page\AbstractPage or Traversable, or an array'
  98. );
  99. }
  100. $page = Page\AbstractPage::factory($page);
  101. }
  102. $hash = $page->hashCode();
  103. if (array_key_exists($hash, $this->index)) {
  104. // page is already in container
  105. return $this;
  106. }
  107. // adds page to container and sets dirty flag
  108. $this->pages[$hash] = $page;
  109. $this->index[$hash] = $page->getOrder();
  110. $this->dirtyIndex = true;
  111. // inject self as page parent
  112. $page->setParent($this);
  113. return $this;
  114. }
  115. /**
  116. * Adds several pages at once
  117. *
  118. * @param array|Traversable|AbstractContainer $pages pages to add
  119. * @return self fluent interface, returns self
  120. * @throws Exception\InvalidArgumentException if $pages is not array,
  121. * Traversable or AbstractContainer
  122. */
  123. public function addPages($pages)
  124. {
  125. if (!is_array($pages) && !$pages instanceof Traversable) {
  126. throw new Exception\InvalidArgumentException(
  127. 'Invalid argument: $pages must be an array, an '
  128. . 'instance of Traversable or an instance of '
  129. . 'Zend\Navigation\AbstractContainer'
  130. );
  131. }
  132. // Because adding a page to a container removes it from the original
  133. // (see {@link Page\AbstractPage::setParent()}), iteration of the
  134. // original container will break. As such, we need to iterate the
  135. // container into an array first.
  136. if ($pages instanceof AbstractContainer) {
  137. $pages = iterator_to_array($pages);
  138. }
  139. foreach ($pages as $page) {
  140. if (null === $page) {
  141. continue;
  142. }
  143. $this->addPage($page);
  144. }
  145. return $this;
  146. }
  147. /**
  148. * Sets pages this container should have, removing existing pages
  149. *
  150. * @param array $pages pages to set
  151. * @return self fluent interface, returns self
  152. */
  153. public function setPages(array $pages)
  154. {
  155. $this->removePages();
  156. return $this->addPages($pages);
  157. }
  158. /**
  159. * Returns pages in the container
  160. *
  161. * @return array array of Page\AbstractPage instances
  162. */
  163. public function getPages()
  164. {
  165. return $this->pages;
  166. }
  167. /**
  168. * Removes the given page from the container
  169. *
  170. * @param Page\AbstractPage|int $page page to remove, either a page
  171. * instance or a specific page order
  172. * @return bool whether the removal was successful
  173. */
  174. public function removePage($page)
  175. {
  176. if ($page instanceof Page\AbstractPage) {
  177. $hash = $page->hashCode();
  178. } elseif (is_int($page)) {
  179. $this->sort();
  180. if (!$hash = array_search($page, $this->index)) {
  181. return false;
  182. }
  183. } else {
  184. return false;
  185. }
  186. if (isset($this->pages[$hash])) {
  187. unset($this->pages[$hash]);
  188. unset($this->index[$hash]);
  189. $this->dirtyIndex = true;
  190. return true;
  191. }
  192. return false;
  193. }
  194. /**
  195. * Removes all pages in container
  196. *
  197. * @return self fluent interface, returns self
  198. */
  199. public function removePages()
  200. {
  201. $this->pages = array();
  202. $this->index = array();
  203. return $this;
  204. }
  205. /**
  206. * Checks if the container has the given page
  207. *
  208. * @param Page\AbstractPage $page page to look for
  209. * @param bool $recursive [optional] whether to search recursively.
  210. * Default is false.
  211. * @return bool whether page is in container
  212. */
  213. public function hasPage(Page\AbstractPage $page, $recursive = false)
  214. {
  215. if (array_key_exists($page->hashCode(), $this->index)) {
  216. return true;
  217. } elseif ($recursive) {
  218. foreach ($this->pages as $childPage) {
  219. if ($childPage->hasPage($page, true)) {
  220. return true;
  221. }
  222. }
  223. }
  224. return false;
  225. }
  226. /**
  227. * Returns true if container contains any pages
  228. *
  229. * @param bool $onlyVisible whether to check only visible pages
  230. * @return bool whether container has any pages
  231. */
  232. public function hasPages($onlyVisible = false)
  233. {
  234. if ($onlyVisible) {
  235. foreach ($this->pages as $page) {
  236. if ($page->isVisible()) {
  237. return true;
  238. }
  239. }
  240. // no visible pages found
  241. return false;
  242. }
  243. return count($this->index) > 0;
  244. }
  245. /**
  246. * Returns a child page matching $property == $value, or null if not found
  247. *
  248. * @param string $property name of property to match against
  249. * @param mixed $value value to match property against
  250. * @return Page\AbstractPage|null matching page or null
  251. */
  252. public function findOneBy($property, $value)
  253. {
  254. $iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST);
  255. foreach ($iterator as $page) {
  256. if ($page->get($property) == $value) {
  257. return $page;
  258. }
  259. }
  260. return null;
  261. }
  262. /**
  263. * Returns all child pages matching $property == $value, or an empty array
  264. * if no pages are found
  265. *
  266. * @param string $property name of property to match against
  267. * @param mixed $value value to match property against
  268. * @return array array containing only Page\AbstractPage instances
  269. */
  270. public function findAllBy($property, $value)
  271. {
  272. $found = array();
  273. $iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST);
  274. foreach ($iterator as $page) {
  275. if ($page->get($property) == $value) {
  276. $found[] = $page;
  277. }
  278. }
  279. return $found;
  280. }
  281. /**
  282. * Returns page(s) matching $property == $value
  283. *
  284. * @param string $property name of property to match against
  285. * @param mixed $value value to match property against
  286. * @param bool $all [optional] whether an array of all matching
  287. * pages should be returned, or only the first.
  288. * If true, an array will be returned, even if not
  289. * matching pages are found. If false, null will
  290. * be returned if no matching page is found.
  291. * Default is false.
  292. * @return Page\AbstractPage|null matching page or null
  293. */
  294. public function findBy($property, $value, $all = false)
  295. {
  296. if ($all) {
  297. return $this->findAllBy($property, $value);
  298. }
  299. return $this->findOneBy($property, $value);
  300. }
  301. /**
  302. * Magic overload: Proxy calls to finder methods
  303. *
  304. * Examples of finder calls:
  305. * <code>
  306. * // METHOD // SAME AS
  307. * $nav->findByLabel('foo'); // $nav->findOneBy('label', 'foo');
  308. * $nav->findOneByLabel('foo'); // $nav->findOneBy('label', 'foo');
  309. * $nav->findAllByClass('foo'); // $nav->findAllBy('class', 'foo');
  310. * </code>
  311. *
  312. * @param string $method method name
  313. * @param array $arguments method arguments
  314. * @throws Exception\BadMethodCallException if method does not exist
  315. */
  316. public function __call($method, $arguments)
  317. {
  318. ErrorHandler::start(E_WARNING);
  319. $result = preg_match('/(find(?:One|All)?By)(.+)/', $method, $match);
  320. $error = ErrorHandler::stop();
  321. if (!$result) {
  322. throw new Exception\BadMethodCallException(sprintf(
  323. 'Bad method call: Unknown method %s::%s',
  324. get_class($this),
  325. $method
  326. ), 0, $error);
  327. }
  328. return $this->{$match[1]}($match[2], $arguments[0]);
  329. }
  330. /**
  331. * Returns an array representation of all pages in container
  332. *
  333. * @return array
  334. */
  335. public function toArray()
  336. {
  337. $this->sort();
  338. $pages = array();
  339. $indexes = array_keys($this->index);
  340. foreach ($indexes as $hash) {
  341. $pages[] = $this->pages[$hash]->toArray();
  342. }
  343. return $pages;
  344. }
  345. // RecursiveIterator interface:
  346. /**
  347. * Returns current page
  348. *
  349. * Implements RecursiveIterator interface.
  350. *
  351. * @return Page\AbstractPage current page or null
  352. * @throws Exception\OutOfBoundsException if the index is invalid
  353. */
  354. public function current()
  355. {
  356. $this->sort();
  357. current($this->index);
  358. $hash = key($this->index);
  359. if (!isset($this->pages[$hash])) {
  360. throw new Exception\OutOfBoundsException(
  361. 'Corruption detected in container; '
  362. . 'invalid key found in internal iterator'
  363. );
  364. }
  365. return $this->pages[$hash];
  366. }
  367. /**
  368. * Returns hash code of current page
  369. *
  370. * Implements RecursiveIterator interface.
  371. *
  372. * @return string hash code of current page
  373. */
  374. public function key()
  375. {
  376. $this->sort();
  377. return key($this->index);
  378. }
  379. /**
  380. * Moves index pointer to next page in the container
  381. *
  382. * Implements RecursiveIterator interface.
  383. *
  384. * @return void
  385. */
  386. public function next()
  387. {
  388. $this->sort();
  389. next($this->index);
  390. }
  391. /**
  392. * Sets index pointer to first page in the container
  393. *
  394. * Implements RecursiveIterator interface.
  395. *
  396. * @return void
  397. */
  398. public function rewind()
  399. {
  400. $this->sort();
  401. reset($this->index);
  402. }
  403. /**
  404. * Checks if container index is valid
  405. *
  406. * Implements RecursiveIterator interface.
  407. *
  408. * @return bool
  409. */
  410. public function valid()
  411. {
  412. $this->sort();
  413. return current($this->index) !== false;
  414. }
  415. /**
  416. * Proxy to hasPages()
  417. *
  418. * Implements RecursiveIterator interface.
  419. *
  420. * @return bool whether container has any pages
  421. */
  422. public function hasChildren()
  423. {
  424. return $this->hasPages();
  425. }
  426. /**
  427. * Returns the child container.
  428. *
  429. * Implements RecursiveIterator interface.
  430. *
  431. * @return Page\AbstractPage|null
  432. */
  433. public function getChildren()
  434. {
  435. $hash = key($this->index);
  436. if (isset($this->pages[$hash])) {
  437. return $this->pages[$hash];
  438. }
  439. return null;
  440. }
  441. // Countable interface:
  442. /**
  443. * Returns number of pages in container
  444. *
  445. * Implements Countable interface.
  446. *
  447. * @return int number of pages in the container
  448. */
  449. public function count()
  450. {
  451. return count($this->index);
  452. }
  453. }