PageCollection.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @package Grav\Common\Flex
  5. *
  6. * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
  7. * @license MIT License; see LICENSE file for details.
  8. */
  9. namespace Grav\Common\Flex\Types\Pages;
  10. use Exception;
  11. use Grav\Common\Flex\Traits\FlexCollectionTrait;
  12. use Grav\Common\Flex\Traits\FlexGravTrait;
  13. use Grav\Common\Grav;
  14. use Grav\Common\Page\Header;
  15. use Grav\Common\Page\Interfaces\PageCollectionInterface;
  16. use Grav\Common\Page\Interfaces\PageInterface;
  17. use Grav\Common\Utils;
  18. use Grav\Framework\Flex\Pages\FlexPageCollection;
  19. use Collator;
  20. use InvalidArgumentException;
  21. use RuntimeException;
  22. use function array_search;
  23. use function count;
  24. use function extension_loaded;
  25. use function in_array;
  26. use function is_array;
  27. use function is_string;
  28. /**
  29. * Class GravPageCollection
  30. * @package Grav\Plugin\FlexObjects\Types\GravPages
  31. *
  32. * @template T as PageObject
  33. * @extends FlexPageCollection<T>
  34. * @implements PageCollectionInterface<string,T>
  35. *
  36. * Incompatibilities with Grav\Common\Page\Collection:
  37. * $page = $collection->key() will not work at all
  38. * $clone = clone $collection does not clone objects inside the collection, does it matter?
  39. * $string = (string)$collection returns collection id instead of comma separated list
  40. * $collection->add() incompatible method signature
  41. * $collection->remove() incompatible method signature
  42. * $collection->filter() incompatible method signature (takes closure instead of callable)
  43. * $collection->prev() does not rewind the internal pointer
  44. * AND most methods are immutable; they do not update the current collection, but return updated one
  45. *
  46. * @method PageIndex getIndex()
  47. */
  48. class PageCollection extends FlexPageCollection implements PageCollectionInterface
  49. {
  50. use FlexGravTrait;
  51. use FlexCollectionTrait;
  52. /** @var array|null */
  53. protected $_params;
  54. /**
  55. * @return array
  56. */
  57. public static function getCachedMethods(): array
  58. {
  59. return [
  60. // Collection specific methods
  61. 'getRoot' => false,
  62. 'getParams' => false,
  63. 'setParams' => false,
  64. 'params' => false,
  65. 'addPage' => false,
  66. 'merge' => false,
  67. 'intersect' => false,
  68. 'prev' => false,
  69. 'nth' => false,
  70. 'random' => false,
  71. 'append' => false,
  72. 'batch' => false,
  73. 'order' => false,
  74. // Collection filtering
  75. 'dateRange' => true,
  76. 'visible' => true,
  77. 'nonVisible' => true,
  78. 'pages' => true,
  79. 'modules' => true,
  80. 'modular' => true,
  81. 'nonModular' => true,
  82. 'published' => true,
  83. 'nonPublished' => true,
  84. 'routable' => true,
  85. 'nonRoutable' => true,
  86. 'ofType' => true,
  87. 'ofOneOfTheseTypes' => true,
  88. 'ofOneOfTheseAccessLevels' => true,
  89. 'withOrdered' => true,
  90. 'withModules' => true,
  91. 'withPages' => true,
  92. 'withTranslation' => true,
  93. 'filterBy' => true,
  94. 'toExtendedArray' => false,
  95. 'getLevelListing' => false,
  96. ] + parent::getCachedMethods();
  97. }
  98. /**
  99. * @return PageInterface
  100. */
  101. public function getRoot()
  102. {
  103. return $this->getIndex()->getRoot();
  104. }
  105. /**
  106. * Get the collection params
  107. *
  108. * @return array
  109. */
  110. public function getParams(): array
  111. {
  112. return $this->_params ?? [];
  113. }
  114. /**
  115. * Set parameters to the Collection
  116. *
  117. * @param array $params
  118. * @return $this
  119. */
  120. public function setParams(array $params)
  121. {
  122. $this->_params = $this->_params ? array_merge($this->_params, $params) : $params;
  123. return $this;
  124. }
  125. /**
  126. * Get the collection params
  127. *
  128. * @return array
  129. */
  130. public function params(): array
  131. {
  132. return $this->getParams();
  133. }
  134. /**
  135. * Add a single page to a collection
  136. *
  137. * @param PageInterface $page
  138. * @return $this
  139. */
  140. public function addPage(PageInterface $page)
  141. {
  142. if (!$page instanceof PageObject) {
  143. throw new InvalidArgumentException('$page is not a flex page.');
  144. }
  145. // FIXME: support other keys.
  146. $this->set($page->getKey(), $page);
  147. return $this;
  148. }
  149. /**
  150. *
  151. * Merge another collection with the current collection
  152. *
  153. * @param PageCollectionInterface $collection
  154. * @return static
  155. * @phpstan-return static<T>
  156. */
  157. public function merge(PageCollectionInterface $collection)
  158. {
  159. throw new RuntimeException(__METHOD__ . '(): Not Implemented');
  160. }
  161. /**
  162. * Intersect another collection with the current collection
  163. *
  164. * @param PageCollectionInterface $collection
  165. * @return static
  166. * @phpstan-return static<T>
  167. */
  168. public function intersect(PageCollectionInterface $collection)
  169. {
  170. throw new RuntimeException(__METHOD__ . '(): Not Implemented');
  171. }
  172. /**
  173. * Set current page.
  174. */
  175. public function setCurrent(string $path): void
  176. {
  177. throw new RuntimeException(__METHOD__ . '(): Not Implemented');
  178. }
  179. /**
  180. * Return previous item.
  181. *
  182. * @return PageInterface|false
  183. * @phpstan-return T|false
  184. */
  185. public function prev()
  186. {
  187. // FIXME: this method does not rewind the internal pointer!
  188. $key = (string)$this->key();
  189. $prev = $this->prevSibling($key);
  190. return $prev !== $this->current() ? $prev : false;
  191. }
  192. /**
  193. * Return nth item.
  194. * @param int $key
  195. * @return PageInterface|bool
  196. * @phpstan-return T|false
  197. */
  198. public function nth($key)
  199. {
  200. return $this->slice($key, 1)[0] ?? false;
  201. }
  202. /**
  203. * Pick one or more random entries.
  204. *
  205. * @param int $num Specifies how many entries should be picked.
  206. * @return static
  207. * @phpstan-return static<T>
  208. */
  209. public function random($num = 1)
  210. {
  211. return $this->createFrom($this->shuffle()->slice(0, $num));
  212. }
  213. /**
  214. * Append new elements to the list.
  215. *
  216. * @param array $items Items to be appended. Existing keys will be overridden with the new values.
  217. * @return static
  218. * @phpstan-return static<T>
  219. */
  220. public function append($items)
  221. {
  222. throw new RuntimeException(__METHOD__ . '(): Not Implemented');
  223. }
  224. /**
  225. * Split collection into array of smaller collections.
  226. *
  227. * @param int $size
  228. * @return static[]
  229. * @phpstan-return static<T>[]
  230. */
  231. public function batch($size): array
  232. {
  233. $chunks = $this->chunk($size);
  234. $list = [];
  235. foreach ($chunks as $chunk) {
  236. $list[] = $this->createFrom($chunk);
  237. }
  238. return $list;
  239. }
  240. /**
  241. * Reorder collection.
  242. *
  243. * @param string $by
  244. * @param string $dir
  245. * @param array|null $manual
  246. * @param int|null $sort_flags
  247. * @return static
  248. * @phpstan-return static<T>
  249. */
  250. public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
  251. {
  252. if (!$this->count()) {
  253. return $this;
  254. }
  255. if ($by === 'random') {
  256. return $this->shuffle();
  257. }
  258. $keys = $this->buildSort($by, $dir, $manual, $sort_flags);
  259. return $this->createFrom(array_replace(array_flip($keys), $this->toArray()) ?? []);
  260. }
  261. /**
  262. * @param string $order_by
  263. * @param string $order_dir
  264. * @param array|null $manual
  265. * @param int|null $sort_flags
  266. * @return array
  267. */
  268. protected function buildSort($order_by = 'default', $order_dir = 'asc', $manual = null, $sort_flags = null): array
  269. {
  270. // do this header query work only once
  271. $header_query = null;
  272. $header_default = null;
  273. if (strpos($order_by, 'header.') === 0) {
  274. $query = explode('|', str_replace('header.', '', $order_by), 2);
  275. $header_query = array_shift($query) ?? '';
  276. $header_default = array_shift($query);
  277. }
  278. $list = [];
  279. foreach ($this as $key => $child) {
  280. switch ($order_by) {
  281. case 'title':
  282. $list[$key] = $child->title();
  283. break;
  284. case 'date':
  285. $list[$key] = $child->date();
  286. $sort_flags = SORT_REGULAR;
  287. break;
  288. case 'modified':
  289. $list[$key] = $child->modified();
  290. $sort_flags = SORT_REGULAR;
  291. break;
  292. case 'publish_date':
  293. $list[$key] = $child->publishDate();
  294. $sort_flags = SORT_REGULAR;
  295. break;
  296. case 'unpublish_date':
  297. $list[$key] = $child->unpublishDate();
  298. $sort_flags = SORT_REGULAR;
  299. break;
  300. case 'slug':
  301. $list[$key] = $child->slug();
  302. break;
  303. case 'basename':
  304. $list[$key] = Utils::basename($key);
  305. break;
  306. case 'folder':
  307. $list[$key] = $child->folder();
  308. break;
  309. case 'manual':
  310. case 'default':
  311. default:
  312. if (is_string($header_query)) {
  313. /** @var Header $child_header */
  314. $child_header = $child->header();
  315. $header_value = $child_header->get($header_query);
  316. if (is_array($header_value)) {
  317. $list[$key] = implode(',', $header_value);
  318. } elseif ($header_value) {
  319. $list[$key] = $header_value;
  320. } else {
  321. $list[$key] = $header_default ?: $key;
  322. }
  323. $sort_flags = $sort_flags ?: SORT_REGULAR;
  324. break;
  325. }
  326. $list[$key] = $key;
  327. $sort_flags = $sort_flags ?: SORT_REGULAR;
  328. }
  329. }
  330. if (null === $sort_flags) {
  331. $sort_flags = SORT_NATURAL | SORT_FLAG_CASE;
  332. }
  333. // else just sort the list according to specified key
  334. if (extension_loaded('intl') && Grav::instance()['config']->get('system.intl_enabled')) {
  335. $locale = setlocale(LC_COLLATE, '0'); //`setlocale` with a '0' param returns the current locale set
  336. $col = Collator::create($locale);
  337. if ($col) {
  338. $col->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
  339. if (($sort_flags & SORT_NATURAL) === SORT_NATURAL) {
  340. $list = preg_replace_callback('~([0-9]+)\.~', static function ($number) {
  341. return sprintf('%032d.', $number[0]);
  342. }, $list);
  343. if (!is_array($list)) {
  344. throw new RuntimeException('Internal Error');
  345. }
  346. $list_vals = array_values($list);
  347. if (is_numeric(array_shift($list_vals))) {
  348. $sort_flags = Collator::SORT_REGULAR;
  349. } else {
  350. $sort_flags = Collator::SORT_STRING;
  351. }
  352. }
  353. $col->asort($list, $sort_flags);
  354. } else {
  355. asort($list, $sort_flags);
  356. }
  357. } else {
  358. asort($list, $sort_flags);
  359. }
  360. // Move manually ordered items into the beginning of the list. Order of the unlisted items does not change.
  361. if (is_array($manual) && !empty($manual)) {
  362. $i = count($manual);
  363. $new_list = [];
  364. foreach ($list as $key => $dummy) {
  365. $child = $this[$key] ?? null;
  366. $order = $child ? array_search($child->slug, $manual, true) : false;
  367. if ($order === false) {
  368. $order = $i++;
  369. }
  370. $new_list[$key] = (int)$order;
  371. }
  372. $list = $new_list;
  373. // Apply manual ordering to the list.
  374. asort($list, SORT_NUMERIC);
  375. }
  376. if ($order_dir !== 'asc') {
  377. $list = array_reverse($list);
  378. }
  379. return array_keys($list);
  380. }
  381. /**
  382. * Mimicks Pages class.
  383. *
  384. * @return $this
  385. * @deprecated 1.7 Not needed anymore in Flex Pages (does nothing).
  386. */
  387. public function all()
  388. {
  389. return $this;
  390. }
  391. /**
  392. * Returns the items between a set of date ranges of either the page date field (default) or
  393. * an arbitrary datetime page field where start date and end date are optional
  394. * Dates must be passed in as text that strtotime() can process
  395. * http://php.net/manual/en/function.strtotime.php
  396. *
  397. * @param string|null $startDate
  398. * @param string|null $endDate
  399. * @param string|null $field
  400. * @return static
  401. * @phpstan-return static<T>
  402. * @throws Exception
  403. */
  404. public function dateRange($startDate = null, $endDate = null, $field = null)
  405. {
  406. $start = $startDate ? Utils::date2timestamp($startDate) : null;
  407. $end = $endDate ? Utils::date2timestamp($endDate) : null;
  408. $entries = [];
  409. foreach ($this as $key => $object) {
  410. if (!$object) {
  411. continue;
  412. }
  413. $date = $field ? strtotime($object->getNestedProperty($field)) : $object->date();
  414. if ((!$start || $date >= $start) && (!$end || $date <= $end)) {
  415. $entries[$key] = $object;
  416. }
  417. }
  418. return $this->createFrom($entries);
  419. }
  420. /**
  421. * Creates new collection with only visible pages
  422. *
  423. * @return static The collection with only visible pages
  424. * @phpstan-return static<T>
  425. */
  426. public function visible()
  427. {
  428. $entries = [];
  429. foreach ($this as $key => $object) {
  430. if ($object && $object->visible()) {
  431. $entries[$key] = $object;
  432. }
  433. }
  434. return $this->createFrom($entries);
  435. }
  436. /**
  437. * Creates new collection with only non-visible pages
  438. *
  439. * @return static The collection with only non-visible pages
  440. * @phpstan-return static<T>
  441. */
  442. public function nonVisible()
  443. {
  444. $entries = [];
  445. foreach ($this as $key => $object) {
  446. if ($object && !$object->visible()) {
  447. $entries[$key] = $object;
  448. }
  449. }
  450. return $this->createFrom($entries);
  451. }
  452. /**
  453. * Creates new collection with only pages
  454. *
  455. * @return static The collection with only pages
  456. * @phpstan-return static<T>
  457. */
  458. public function pages()
  459. {
  460. $entries = [];
  461. /**
  462. * @var int|string $key
  463. * @var PageInterface|null $object
  464. */
  465. foreach ($this as $key => $object) {
  466. if ($object && !$object->isModule()) {
  467. $entries[$key] = $object;
  468. }
  469. }
  470. return $this->createFrom($entries);
  471. }
  472. /**
  473. * Creates new collection with only modules
  474. *
  475. * @return static The collection with only modules
  476. * @phpstan-return static<T>
  477. */
  478. public function modules()
  479. {
  480. $entries = [];
  481. /**
  482. * @var int|string $key
  483. * @var PageInterface|null $object
  484. */
  485. foreach ($this as $key => $object) {
  486. if ($object && $object->isModule()) {
  487. $entries[$key] = $object;
  488. }
  489. }
  490. return $this->createFrom($entries);
  491. }
  492. /**
  493. * Alias of modules()
  494. *
  495. * @return static
  496. * @phpstan-return static<T>
  497. */
  498. public function modular()
  499. {
  500. return $this->modules();
  501. }
  502. /**
  503. * Alias of pages()
  504. *
  505. * @return static
  506. * @phpstan-return static<T>
  507. */
  508. public function nonModular()
  509. {
  510. return $this->pages();
  511. }
  512. /**
  513. * Creates new collection with only published pages
  514. *
  515. * @return static The collection with only published pages
  516. * @phpstan-return static<T>
  517. */
  518. public function published()
  519. {
  520. $entries = [];
  521. foreach ($this as $key => $object) {
  522. if ($object && $object->published()) {
  523. $entries[$key] = $object;
  524. }
  525. }
  526. return $this->createFrom($entries);
  527. }
  528. /**
  529. * Creates new collection with only non-published pages
  530. *
  531. * @return static The collection with only non-published pages
  532. * @phpstan-return static<T>
  533. */
  534. public function nonPublished()
  535. {
  536. $entries = [];
  537. foreach ($this as $key => $object) {
  538. if ($object && !$object->published()) {
  539. $entries[$key] = $object;
  540. }
  541. }
  542. return $this->createFrom($entries);
  543. }
  544. /**
  545. * Creates new collection with only routable pages
  546. *
  547. * @return static The collection with only routable pages
  548. * @phpstan-return static<T>
  549. */
  550. public function routable()
  551. {
  552. $entries = [];
  553. foreach ($this as $key => $object) {
  554. if ($object && $object->routable()) {
  555. $entries[$key] = $object;
  556. }
  557. }
  558. return $this->createFrom($entries);
  559. }
  560. /**
  561. * Creates new collection with only non-routable pages
  562. *
  563. * @return static The collection with only non-routable pages
  564. * @phpstan-return static<T>
  565. */
  566. public function nonRoutable()
  567. {
  568. $entries = [];
  569. foreach ($this as $key => $object) {
  570. if ($object && !$object->routable()) {
  571. $entries[$key] = $object;
  572. }
  573. }
  574. return $this->createFrom($entries);
  575. }
  576. /**
  577. * Creates new collection with only pages of the specified type
  578. *
  579. * @param string $type
  580. * @return static The collection
  581. * @phpstan-return static<T>
  582. */
  583. public function ofType($type)
  584. {
  585. $entries = [];
  586. foreach ($this as $key => $object) {
  587. if ($object && $object->template() === $type) {
  588. $entries[$key] = $object;
  589. }
  590. }
  591. return $this->createFrom($entries);
  592. }
  593. /**
  594. * Creates new collection with only pages of one of the specified types
  595. *
  596. * @param string[] $types
  597. * @return static The collection
  598. * @phpstan-return static<T>
  599. */
  600. public function ofOneOfTheseTypes($types)
  601. {
  602. $entries = [];
  603. foreach ($this as $key => $object) {
  604. if ($object && in_array($object->template(), $types, true)) {
  605. $entries[$key] = $object;
  606. }
  607. }
  608. return $this->createFrom($entries);
  609. }
  610. /**
  611. * Creates new collection with only pages of one of the specified access levels
  612. *
  613. * @param array $accessLevels
  614. * @return static The collection
  615. * @phpstan-return static<T>
  616. */
  617. public function ofOneOfTheseAccessLevels($accessLevels)
  618. {
  619. $entries = [];
  620. foreach ($this as $key => $object) {
  621. if ($object && isset($object->header()->access)) {
  622. if (is_array($object->header()->access)) {
  623. //Multiple values for access
  624. $valid = false;
  625. foreach ($object->header()->access as $index => $accessLevel) {
  626. if (is_array($accessLevel)) {
  627. foreach ($accessLevel as $innerIndex => $innerAccessLevel) {
  628. if (in_array($innerAccessLevel, $accessLevels)) {
  629. $valid = true;
  630. }
  631. }
  632. } else {
  633. if (in_array($index, $accessLevels)) {
  634. $valid = true;
  635. }
  636. }
  637. }
  638. if ($valid) {
  639. $entries[$key] = $object;
  640. }
  641. } else {
  642. //Single value for access
  643. if (in_array($object->header()->access, $accessLevels)) {
  644. $entries[$key] = $object;
  645. }
  646. }
  647. }
  648. }
  649. return $this->createFrom($entries);
  650. }
  651. /**
  652. * @param bool $bool
  653. * @return static
  654. * @phpstan-return static<T>
  655. */
  656. public function withOrdered(bool $bool = true)
  657. {
  658. $list = array_keys(array_filter($this->call('isOrdered', [$bool])));
  659. return $this->select($list);
  660. }
  661. /**
  662. * @param bool $bool
  663. * @return static
  664. * @phpstan-return static<T>
  665. */
  666. public function withModules(bool $bool = true)
  667. {
  668. $list = array_keys(array_filter($this->call('isModule', [$bool])));
  669. return $this->select($list);
  670. }
  671. /**
  672. * @param bool $bool
  673. * @return static
  674. * @phpstan-return static<T>
  675. */
  676. public function withPages(bool $bool = true)
  677. {
  678. $list = array_keys(array_filter($this->call('isPage', [$bool])));
  679. return $this->select($list);
  680. }
  681. /**
  682. * @param bool $bool
  683. * @param string|null $languageCode
  684. * @param bool|null $fallback
  685. * @return static
  686. * @phpstan-return static<T>
  687. */
  688. public function withTranslation(bool $bool = true, string $languageCode = null, bool $fallback = null)
  689. {
  690. $list = array_keys(array_filter($this->call('hasTranslation', [$languageCode, $fallback])));
  691. return $bool ? $this->select($list) : $this->unselect($list);
  692. }
  693. /**
  694. * @param string|null $languageCode
  695. * @param bool|null $fallback
  696. * @return PageIndex
  697. */
  698. public function withTranslated(string $languageCode = null, bool $fallback = null)
  699. {
  700. return $this->getIndex()->withTranslated($languageCode, $fallback);
  701. }
  702. /**
  703. * Filter pages by given filters.
  704. *
  705. * - search: string
  706. * - page_type: string|string[]
  707. * - modular: bool
  708. * - visible: bool
  709. * - routable: bool
  710. * - published: bool
  711. * - page: bool
  712. * - translated: bool
  713. *
  714. * @param array $filters
  715. * @param bool $recursive
  716. * @return static
  717. * @phpstan-return static<T>
  718. */
  719. public function filterBy(array $filters, bool $recursive = false)
  720. {
  721. $list = array_keys(array_filter($this->call('filterBy', [$filters, $recursive])));
  722. return $this->select($list);
  723. }
  724. /**
  725. * Get the extended version of this Collection with each page keyed by route
  726. *
  727. * @return array
  728. * @throws Exception
  729. */
  730. public function toExtendedArray(): array
  731. {
  732. $entries = [];
  733. foreach ($this as $key => $object) {
  734. if ($object) {
  735. $entries[$object->route()] = $object->toArray();
  736. }
  737. }
  738. return $entries;
  739. }
  740. /**
  741. * @param array $options
  742. * @return array
  743. */
  744. public function getLevelListing(array $options): array
  745. {
  746. /** @var PageIndex $index */
  747. $index = $this->getIndex();
  748. return method_exists($index, 'getLevelListing') ? $index->getLevelListing($options) : [];
  749. }
  750. }