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