PageRenderTime 25ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/library/Zend/Navigation/AbstractContainer.php

https://github.com/Ocramius/zf2
PHP | 521 lines | 252 code | 58 blank | 211 comment | 31 complexity | d8b8d24cf4460fe1f6a89ff3d58fff1c 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-2015 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. * @param bool $recursive [optional] whether to remove recursively
  173. * @return bool whether the removal was successful
  174. */
  175. public function removePage($page, $recursive = false)
  176. {
  177. if ($page instanceof Page\AbstractPage) {
  178. $hash = $page->hashCode();
  179. } elseif (is_int($page)) {
  180. $this->sort();
  181. if (!$hash = array_search($page, $this->index)) {
  182. return false;
  183. }
  184. } else {
  185. return false;
  186. }
  187. if (isset($this->pages[$hash])) {
  188. unset($this->pages[$hash]);
  189. unset($this->index[$hash]);
  190. $this->dirtyIndex = true;
  191. return true;
  192. }
  193. if ($recursive) {
  194. /** @var \Zend\Navigation\Page\AbstractPage $childPage */
  195. foreach ($this->pages as $childPage) {
  196. if ($childPage->hasPage($page, true)) {
  197. $childPage->removePage($page, true);
  198. return true;
  199. }
  200. }
  201. }
  202. return false;
  203. }
  204. /**
  205. * Removes all pages in container
  206. *
  207. * @return self fluent interface, returns self
  208. */
  209. public function removePages()
  210. {
  211. $this->pages = array();
  212. $this->index = array();
  213. return $this;
  214. }
  215. /**
  216. * Checks if the container has the given page
  217. *
  218. * @param Page\AbstractPage $page page to look for
  219. * @param bool $recursive [optional] whether to search recursively.
  220. * Default is false.
  221. * @return bool whether page is in container
  222. */
  223. public function hasPage(Page\AbstractPage $page, $recursive = false)
  224. {
  225. if (array_key_exists($page->hashCode(), $this->index)) {
  226. return true;
  227. } elseif ($recursive) {
  228. foreach ($this->pages as $childPage) {
  229. if ($childPage->hasPage($page, true)) {
  230. return true;
  231. }
  232. }
  233. }
  234. return false;
  235. }
  236. /**
  237. * Returns true if container contains any pages
  238. *
  239. * @param bool $onlyVisible whether to check only visible pages
  240. * @return bool whether container has any pages
  241. */
  242. public function hasPages($onlyVisible = false)
  243. {
  244. if ($onlyVisible) {
  245. foreach ($this->pages as $page) {
  246. if ($page->isVisible()) {
  247. return true;
  248. }
  249. }
  250. // no visible pages found
  251. return false;
  252. }
  253. return count($this->index) > 0;
  254. }
  255. /**
  256. * Returns a child page matching $property == $value, or null if not found
  257. *
  258. * @param string $property name of property to match against
  259. * @param mixed $value value to match property against
  260. * @return Page\AbstractPage|null matching page or null
  261. */
  262. public function findOneBy($property, $value)
  263. {
  264. $iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST);
  265. foreach ($iterator as $page) {
  266. if ($page->get($property) == $value) {
  267. return $page;
  268. }
  269. }
  270. return null;
  271. }
  272. /**
  273. * Returns all child pages matching $property == $value, or an empty array
  274. * if no pages are found
  275. *
  276. * @param string $property name of property to match against
  277. * @param mixed $value value to match property against
  278. * @return array array containing only Page\AbstractPage instances
  279. */
  280. public function findAllBy($property, $value)
  281. {
  282. $found = array();
  283. $iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST);
  284. foreach ($iterator as $page) {
  285. if ($page->get($property) == $value) {
  286. $found[] = $page;
  287. }
  288. }
  289. return $found;
  290. }
  291. /**
  292. * Returns page(s) matching $property == $value
  293. *
  294. * @param string $property name of property to match against
  295. * @param mixed $value value to match property against
  296. * @param bool $all [optional] whether an array of all matching
  297. * pages should be returned, or only the first.
  298. * If true, an array will be returned, even if not
  299. * matching pages are found. If false, null will
  300. * be returned if no matching page is found.
  301. * Default is false.
  302. * @return Page\AbstractPage|null matching page or null
  303. */
  304. public function findBy($property, $value, $all = false)
  305. {
  306. if ($all) {
  307. return $this->findAllBy($property, $value);
  308. }
  309. return $this->findOneBy($property, $value);
  310. }
  311. /**
  312. * Magic overload: Proxy calls to finder methods
  313. *
  314. * Examples of finder calls:
  315. * <code>
  316. * // METHOD // SAME AS
  317. * $nav->findByLabel('foo'); // $nav->findOneBy('label', 'foo');
  318. * $nav->findOneByLabel('foo'); // $nav->findOneBy('label', 'foo');
  319. * $nav->findAllByClass('foo'); // $nav->findAllBy('class', 'foo');
  320. * </code>
  321. *
  322. * @param string $method method name
  323. * @param array $arguments method arguments
  324. * @throws Exception\BadMethodCallException if method does not exist
  325. */
  326. public function __call($method, $arguments)
  327. {
  328. ErrorHandler::start(E_WARNING);
  329. $result = preg_match('/(find(?:One|All)?By)(.+)/', $method, $match);
  330. $error = ErrorHandler::stop();
  331. if (!$result) {
  332. throw new Exception\BadMethodCallException(sprintf(
  333. 'Bad method call: Unknown method %s::%s',
  334. get_class($this),
  335. $method
  336. ), 0, $error);
  337. }
  338. return $this->{$match[1]}($match[2], $arguments[0]);
  339. }
  340. /**
  341. * Returns an array representation of all pages in container
  342. *
  343. * @return array
  344. */
  345. public function toArray()
  346. {
  347. $this->sort();
  348. $pages = array();
  349. $indexes = array_keys($this->index);
  350. foreach ($indexes as $hash) {
  351. $pages[] = $this->pages[$hash]->toArray();
  352. }
  353. return $pages;
  354. }
  355. // RecursiveIterator interface:
  356. /**
  357. * Returns current page
  358. *
  359. * Implements RecursiveIterator interface.
  360. *
  361. * @return Page\AbstractPage current page or null
  362. * @throws Exception\OutOfBoundsException if the index is invalid
  363. */
  364. public function current()
  365. {
  366. $this->sort();
  367. current($this->index);
  368. $hash = key($this->index);
  369. if (!isset($this->pages[$hash])) {
  370. throw new Exception\OutOfBoundsException(
  371. 'Corruption detected in container; '
  372. . 'invalid key found in internal iterator'
  373. );
  374. }
  375. return $this->pages[$hash];
  376. }
  377. /**
  378. * Returns hash code of current page
  379. *
  380. * Implements RecursiveIterator interface.
  381. *
  382. * @return string hash code of current page
  383. */
  384. public function key()
  385. {
  386. $this->sort();
  387. return key($this->index);
  388. }
  389. /**
  390. * Moves index pointer to next page in the container
  391. *
  392. * Implements RecursiveIterator interface.
  393. *
  394. * @return void
  395. */
  396. public function next()
  397. {
  398. $this->sort();
  399. next($this->index);
  400. }
  401. /**
  402. * Sets index pointer to first page in the container
  403. *
  404. * Implements RecursiveIterator interface.
  405. *
  406. * @return void
  407. */
  408. public function rewind()
  409. {
  410. $this->sort();
  411. reset($this->index);
  412. }
  413. /**
  414. * Checks if container index is valid
  415. *
  416. * Implements RecursiveIterator interface.
  417. *
  418. * @return bool
  419. */
  420. public function valid()
  421. {
  422. $this->sort();
  423. return current($this->index) !== false;
  424. }
  425. /**
  426. * Proxy to hasPages()
  427. *
  428. * Implements RecursiveIterator interface.
  429. *
  430. * @return bool whether container has any pages
  431. */
  432. public function hasChildren()
  433. {
  434. return $this->valid() && $this->current()->hasPages();
  435. }
  436. /**
  437. * Returns the child container.
  438. *
  439. * Implements RecursiveIterator interface.
  440. *
  441. * @return Page\AbstractPage|null
  442. */
  443. public function getChildren()
  444. {
  445. $hash = key($this->index);
  446. if (isset($this->pages[$hash])) {
  447. return $this->pages[$hash];
  448. }
  449. return null;
  450. }
  451. // Countable interface:
  452. /**
  453. * Returns number of pages in container
  454. *
  455. * Implements Countable interface.
  456. *
  457. * @return int number of pages in the container
  458. */
  459. public function count()
  460. {
  461. return count($this->index);
  462. }
  463. }