Collection.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. <?php
  2. /**
  3. * @package Grav\Common\Page
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Page;
  9. use Grav\Common\Grav;
  10. use Grav\Common\Iterator;
  11. use Grav\Common\Page\Interfaces\PageInterface;
  12. use Grav\Common\Utils;
  13. class Collection extends Iterator
  14. {
  15. /**
  16. * @var Pages
  17. */
  18. protected $pages;
  19. /**
  20. * @var array
  21. */
  22. protected $params;
  23. /**
  24. * Collection constructor.
  25. *
  26. * @param array $items
  27. * @param array $params
  28. * @param Pages|null $pages
  29. */
  30. public function __construct($items = [], array $params = [], Pages $pages = null)
  31. {
  32. parent::__construct($items);
  33. $this->params = $params;
  34. $this->pages = $pages ? $pages : Grav::instance()->offsetGet('pages');
  35. }
  36. /**
  37. * Get the collection params
  38. *
  39. * @return array
  40. */
  41. public function params()
  42. {
  43. return $this->params;
  44. }
  45. /**
  46. * Add a single page to a collection
  47. *
  48. * @param PageInterface $page
  49. *
  50. * @return $this
  51. */
  52. public function addPage(PageInterface $page)
  53. {
  54. $this->items[$page->path()] = ['slug' => $page->slug()];
  55. return $this;
  56. }
  57. /**
  58. * Add a page with path and slug
  59. *
  60. * @param string $path
  61. * @param string $slug
  62. * @return $this
  63. */
  64. public function add($path, $slug)
  65. {
  66. $this->items[$path] = ['slug' => $slug];
  67. return $this;
  68. }
  69. /**
  70. *
  71. * Create a copy of this collection
  72. *
  73. * @return static
  74. */
  75. public function copy()
  76. {
  77. return new static($this->items, $this->params, $this->pages);
  78. }
  79. /**
  80. *
  81. * Merge another collection with the current collection
  82. *
  83. * @param Collection $collection
  84. * @return $this
  85. */
  86. public function merge(Collection $collection)
  87. {
  88. foreach($collection as $page) {
  89. $this->addPage($page);
  90. }
  91. return $this;
  92. }
  93. /**
  94. * Intersect another collection with the current collection
  95. *
  96. * @param Collection $collection
  97. * @return $this
  98. */
  99. public function intersect(Collection $collection)
  100. {
  101. $array1 = $this->items;
  102. $array2 = $collection->toArray();
  103. $this->items = array_uintersect($array1, $array2, function($val1, $val2) {
  104. return strcmp($val1['slug'], $val2['slug']);
  105. });
  106. return $this;
  107. }
  108. /**
  109. * Set parameters to the Collection
  110. *
  111. * @param array $params
  112. *
  113. * @return $this
  114. */
  115. public function setParams(array $params)
  116. {
  117. $this->params = array_merge($this->params, $params);
  118. return $this;
  119. }
  120. /**
  121. * Returns current page.
  122. *
  123. * @return PageInterface
  124. */
  125. public function current()
  126. {
  127. $current = parent::key();
  128. return $this->pages->get($current);
  129. }
  130. /**
  131. * Returns current slug.
  132. *
  133. * @return mixed
  134. */
  135. public function key()
  136. {
  137. $current = parent::current();
  138. return $current['slug'];
  139. }
  140. /**
  141. * Returns the value at specified offset.
  142. *
  143. * @param mixed $offset The offset to retrieve.
  144. *
  145. * @return mixed Can return all value types.
  146. */
  147. public function offsetGet($offset)
  148. {
  149. return $this->pages->get($offset) ?: null;
  150. }
  151. /**
  152. * Split collection into array of smaller collections.
  153. *
  154. * @param int $size
  155. * @return array|Collection[]
  156. */
  157. public function batch($size)
  158. {
  159. $chunks = array_chunk($this->items, $size, true);
  160. $list = [];
  161. foreach ($chunks as $chunk) {
  162. $list[] = new static($chunk, $this->params, $this->pages);
  163. }
  164. return $list;
  165. }
  166. /**
  167. * Remove item from the list.
  168. *
  169. * @param PageInterface|string|null $key
  170. *
  171. * @return $this
  172. * @throws \InvalidArgumentException
  173. */
  174. public function remove($key = null)
  175. {
  176. if ($key instanceof PageInterface) {
  177. $key = $key->path();
  178. } elseif (null === $key) {
  179. $key = (string)key($this->items);
  180. }
  181. if (!\is_string($key)) {
  182. throw new \InvalidArgumentException('Invalid argument $key.');
  183. }
  184. parent::remove($key);
  185. return $this;
  186. }
  187. /**
  188. * Reorder collection.
  189. *
  190. * @param string $by
  191. * @param string $dir
  192. * @param array $manual
  193. * @param string $sort_flags
  194. *
  195. * @return $this
  196. */
  197. public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
  198. {
  199. $this->items = $this->pages->sortCollection($this, $by, $dir, $manual, $sort_flags);
  200. return $this;
  201. }
  202. /**
  203. * Check to see if this item is the first in the collection.
  204. *
  205. * @param string $path
  206. *
  207. * @return bool True if item is first.
  208. */
  209. public function isFirst($path)
  210. {
  211. return $this->items && $path === array_keys($this->items)[0];
  212. }
  213. /**
  214. * Check to see if this item is the last in the collection.
  215. *
  216. * @param string $path
  217. *
  218. * @return bool True if item is last.
  219. */
  220. public function isLast($path)
  221. {
  222. return $this->items && $path === array_keys($this->items)[\count($this->items) - 1];
  223. }
  224. /**
  225. * Gets the previous sibling based on current position.
  226. *
  227. * @param string $path
  228. *
  229. * @return PageInterface The previous item.
  230. */
  231. public function prevSibling($path)
  232. {
  233. return $this->adjacentSibling($path, -1);
  234. }
  235. /**
  236. * Gets the next sibling based on current position.
  237. *
  238. * @param string $path
  239. *
  240. * @return PageInterface The next item.
  241. */
  242. public function nextSibling($path)
  243. {
  244. return $this->adjacentSibling($path, 1);
  245. }
  246. /**
  247. * Returns the adjacent sibling based on a direction.
  248. *
  249. * @param string $path
  250. * @param int $direction either -1 or +1
  251. *
  252. * @return PageInterface|Collection The sibling item.
  253. */
  254. public function adjacentSibling($path, $direction = 1)
  255. {
  256. $values = array_keys($this->items);
  257. $keys = array_flip($values);
  258. if (array_key_exists($path, $keys)) {
  259. $index = $keys[$path] - $direction;
  260. return isset($values[$index]) ? $this->offsetGet($values[$index]) : $this;
  261. }
  262. return $this;
  263. }
  264. /**
  265. * Returns the item in the current position.
  266. *
  267. * @param string $path the path the item
  268. *
  269. * @return int the index of the current page.
  270. */
  271. public function currentPosition($path)
  272. {
  273. return \array_search($path, \array_keys($this->items), true);
  274. }
  275. /**
  276. * Returns the items between a set of date ranges of either the page date field (default) or
  277. * an arbitrary datetime page field where end date is optional
  278. * Dates can be passed in as text that strtotime() can process
  279. * http://php.net/manual/en/function.strtotime.php
  280. *
  281. * @param string $startDate
  282. * @param bool $endDate
  283. * @param string|null $field
  284. *
  285. * @return $this
  286. * @throws \Exception
  287. */
  288. public function dateRange($startDate, $endDate = false, $field = null)
  289. {
  290. $start = Utils::date2timestamp($startDate);
  291. $end = $endDate ? Utils::date2timestamp($endDate) : false;
  292. $date_range = [];
  293. foreach ($this->items as $path => $slug) {
  294. $page = $this->pages->get($path);
  295. if ($page !== null) {
  296. $date = $field ? strtotime($page->value($field)) : $page->date();
  297. if ($date >= $start && (!$end || $date <= $end)) {
  298. $date_range[$path] = $slug;
  299. }
  300. }
  301. }
  302. $this->items = $date_range;
  303. return $this;
  304. }
  305. /**
  306. * Creates new collection with only visible pages
  307. *
  308. * @return Collection The collection with only visible pages
  309. */
  310. public function visible()
  311. {
  312. $visible = [];
  313. foreach ($this->items as $path => $slug) {
  314. $page = $this->pages->get($path);
  315. if ($page !== null && $page->visible()) {
  316. $visible[$path] = $slug;
  317. }
  318. }
  319. $this->items = $visible;
  320. return $this;
  321. }
  322. /**
  323. * Creates new collection with only non-visible pages
  324. *
  325. * @return Collection The collection with only non-visible pages
  326. */
  327. public function nonVisible()
  328. {
  329. $visible = [];
  330. foreach ($this->items as $path => $slug) {
  331. $page = $this->pages->get($path);
  332. if ($page !== null && !$page->visible()) {
  333. $visible[$path] = $slug;
  334. }
  335. }
  336. $this->items = $visible;
  337. return $this;
  338. }
  339. /**
  340. * Creates new collection with only modular pages
  341. *
  342. * @return Collection The collection with only modular pages
  343. */
  344. public function modular()
  345. {
  346. $modular = [];
  347. foreach ($this->items as $path => $slug) {
  348. $page = $this->pages->get($path);
  349. if ($page !== null && $page->modular()) {
  350. $modular[$path] = $slug;
  351. }
  352. }
  353. $this->items = $modular;
  354. return $this;
  355. }
  356. /**
  357. * Creates new collection with only non-modular pages
  358. *
  359. * @return Collection The collection with only non-modular pages
  360. */
  361. public function nonModular()
  362. {
  363. $modular = [];
  364. foreach ($this->items as $path => $slug) {
  365. $page = $this->pages->get($path);
  366. if ($page !== null && !$page->modular()) {
  367. $modular[$path] = $slug;
  368. }
  369. }
  370. $this->items = $modular;
  371. return $this;
  372. }
  373. /**
  374. * Creates new collection with only published pages
  375. *
  376. * @return Collection The collection with only published pages
  377. */
  378. public function published()
  379. {
  380. $published = [];
  381. foreach ($this->items as $path => $slug) {
  382. $page = $this->pages->get($path);
  383. if ($page !== null && $page->published()) {
  384. $published[$path] = $slug;
  385. }
  386. }
  387. $this->items = $published;
  388. return $this;
  389. }
  390. /**
  391. * Creates new collection with only non-published pages
  392. *
  393. * @return Collection The collection with only non-published pages
  394. */
  395. public function nonPublished()
  396. {
  397. $published = [];
  398. foreach ($this->items as $path => $slug) {
  399. $page = $this->pages->get($path);
  400. if ($page !== null && !$page->published()) {
  401. $published[$path] = $slug;
  402. }
  403. }
  404. $this->items = $published;
  405. return $this;
  406. }
  407. /**
  408. * Creates new collection with only routable pages
  409. *
  410. * @return Collection The collection with only routable pages
  411. */
  412. public function routable()
  413. {
  414. $routable = [];
  415. foreach ($this->items as $path => $slug) {
  416. $page = $this->pages->get($path);
  417. if ($page !== null && $page->routable()) {
  418. $routable[$path] = $slug;
  419. }
  420. }
  421. $this->items = $routable;
  422. return $this;
  423. }
  424. /**
  425. * Creates new collection with only non-routable pages
  426. *
  427. * @return Collection The collection with only non-routable pages
  428. */
  429. public function nonRoutable()
  430. {
  431. $routable = [];
  432. foreach ($this->items as $path => $slug) {
  433. $page = $this->pages->get($path);
  434. if ($page !== null && !$page->routable()) {
  435. $routable[$path] = $slug;
  436. }
  437. }
  438. $this->items = $routable;
  439. return $this;
  440. }
  441. /**
  442. * Creates new collection with only pages of the specified type
  443. *
  444. * @param string $type
  445. *
  446. * @return Collection The collection
  447. */
  448. public function ofType($type)
  449. {
  450. $items = [];
  451. foreach ($this->items as $path => $slug) {
  452. $page = $this->pages->get($path);
  453. if ($page !== null && $page->template() === $type) {
  454. $items[$path] = $slug;
  455. }
  456. }
  457. $this->items = $items;
  458. return $this;
  459. }
  460. /**
  461. * Creates new collection with only pages of one of the specified types
  462. *
  463. * @param string[] $types
  464. *
  465. * @return Collection The collection
  466. */
  467. public function ofOneOfTheseTypes($types)
  468. {
  469. $items = [];
  470. foreach ($this->items as $path => $slug) {
  471. $page = $this->pages->get($path);
  472. if ($page !== null && \in_array($page->template(), $types, true)) {
  473. $items[$path] = $slug;
  474. }
  475. }
  476. $this->items = $items;
  477. return $this;
  478. }
  479. /**
  480. * Creates new collection with only pages of one of the specified access levels
  481. *
  482. * @param array $accessLevels
  483. *
  484. * @return Collection The collection
  485. */
  486. public function ofOneOfTheseAccessLevels($accessLevels)
  487. {
  488. $items = [];
  489. foreach ($this->items as $path => $slug) {
  490. $page = $this->pages->get($path);
  491. if ($page !== null && isset($page->header()->access)) {
  492. if (\is_array($page->header()->access)) {
  493. //Multiple values for access
  494. $valid = false;
  495. foreach ($page->header()->access as $index => $accessLevel) {
  496. if (\is_array($accessLevel)) {
  497. foreach ($accessLevel as $innerIndex => $innerAccessLevel) {
  498. if (\in_array($innerAccessLevel, $accessLevels)) {
  499. $valid = true;
  500. }
  501. }
  502. } else {
  503. if (\in_array($index, $accessLevels)) {
  504. $valid = true;
  505. }
  506. }
  507. }
  508. if ($valid) {
  509. $items[$path] = $slug;
  510. }
  511. } else {
  512. //Single value for access
  513. if (\in_array($page->header()->access, $accessLevels)) {
  514. $items[$path] = $slug;
  515. }
  516. }
  517. }
  518. }
  519. $this->items = $items;
  520. return $this;
  521. }
  522. /**
  523. * Get the extended version of this Collection with each page keyed by route
  524. *
  525. * @return array
  526. * @throws \Exception
  527. */
  528. public function toExtendedArray()
  529. {
  530. $items = [];
  531. foreach ($this->items as $path => $slug) {
  532. $page = $this->pages->get($path);
  533. if ($page !== null) {
  534. $items[$page->route()] = $page->toArray();
  535. }
  536. }
  537. return $items;
  538. }
  539. }