Collection.php 17 KB

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