123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839 |
- <?php
- declare(strict_types=1);
- /**
- * @package Grav\Common\Flex
- *
- * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
- * @license MIT License; see LICENSE file for details.
- */
- namespace Grav\Common\Flex\Types\Pages;
- use Exception;
- use Grav\Common\Flex\Traits\FlexCollectionTrait;
- use Grav\Common\Flex\Traits\FlexGravTrait;
- use Grav\Common\Grav;
- use Grav\Common\Page\Header;
- use Grav\Common\Page\Interfaces\PageCollectionInterface;
- use Grav\Common\Page\Interfaces\PageInterface;
- use Grav\Common\Utils;
- use Grav\Framework\Flex\Pages\FlexPageCollection;
- use Collator;
- use InvalidArgumentException;
- use RuntimeException;
- use function array_search;
- use function count;
- use function extension_loaded;
- use function in_array;
- use function is_array;
- use function is_string;
- /**
- * Class GravPageCollection
- * @package Grav\Plugin\FlexObjects\Types\GravPages
- *
- * @template T as PageObject
- * @extends FlexPageCollection<T>
- * @implements PageCollectionInterface<string,T>
- *
- * Incompatibilities with Grav\Common\Page\Collection:
- * $page = $collection->key() will not work at all
- * $clone = clone $collection does not clone objects inside the collection, does it matter?
- * $string = (string)$collection returns collection id instead of comma separated list
- * $collection->add() incompatible method signature
- * $collection->remove() incompatible method signature
- * $collection->filter() incompatible method signature (takes closure instead of callable)
- * $collection->prev() does not rewind the internal pointer
- * AND most methods are immutable; they do not update the current collection, but return updated one
- *
- * @method PageIndex getIndex()
- */
- class PageCollection extends FlexPageCollection implements PageCollectionInterface
- {
- use FlexGravTrait;
- use FlexCollectionTrait;
- /** @var array|null */
- protected $_params;
- /**
- * @return array
- */
- public static function getCachedMethods(): array
- {
- return [
- // Collection specific methods
- 'getRoot' => false,
- 'getParams' => false,
- 'setParams' => false,
- 'params' => false,
- 'addPage' => false,
- 'merge' => false,
- 'intersect' => false,
- 'prev' => false,
- 'nth' => false,
- 'random' => false,
- 'append' => false,
- 'batch' => false,
- 'order' => false,
- // Collection filtering
- 'dateRange' => true,
- 'visible' => true,
- 'nonVisible' => true,
- 'pages' => true,
- 'modules' => true,
- 'modular' => true,
- 'nonModular' => true,
- 'published' => true,
- 'nonPublished' => true,
- 'routable' => true,
- 'nonRoutable' => true,
- 'ofType' => true,
- 'ofOneOfTheseTypes' => true,
- 'ofOneOfTheseAccessLevels' => true,
- 'withOrdered' => true,
- 'withModules' => true,
- 'withPages' => true,
- 'withTranslation' => true,
- 'filterBy' => true,
- 'toExtendedArray' => false,
- 'getLevelListing' => false,
- ] + parent::getCachedMethods();
- }
- /**
- * @return PageInterface
- */
- public function getRoot()
- {
- return $this->getIndex()->getRoot();
- }
- /**
- * Get the collection params
- *
- * @return array
- */
- public function getParams(): array
- {
- return $this->_params ?? [];
- }
- /**
- * Set parameters to the Collection
- *
- * @param array $params
- * @return $this
- */
- public function setParams(array $params)
- {
- $this->_params = $this->_params ? array_merge($this->_params, $params) : $params;
- return $this;
- }
- /**
- * Get the collection params
- *
- * @return array
- */
- public function params(): array
- {
- return $this->getParams();
- }
- /**
- * Add a single page to a collection
- *
- * @param PageInterface $page
- * @return $this
- */
- public function addPage(PageInterface $page)
- {
- if (!$page instanceof PageObject) {
- throw new InvalidArgumentException('$page is not a flex page.');
- }
- // FIXME: support other keys.
- $this->set($page->getKey(), $page);
- return $this;
- }
- /**
- *
- * Merge another collection with the current collection
- *
- * @param PageCollectionInterface $collection
- * @return static
- * @phpstan-return static<T>
- */
- public function merge(PageCollectionInterface $collection)
- {
- throw new RuntimeException(__METHOD__ . '(): Not Implemented');
- }
- /**
- * Intersect another collection with the current collection
- *
- * @param PageCollectionInterface $collection
- * @return static
- * @phpstan-return static<T>
- */
- public function intersect(PageCollectionInterface $collection)
- {
- throw new RuntimeException(__METHOD__ . '(): Not Implemented');
- }
- /**
- * Set current page.
- */
- public function setCurrent(string $path): void
- {
- throw new RuntimeException(__METHOD__ . '(): Not Implemented');
- }
- /**
- * Return previous item.
- *
- * @return PageInterface|false
- * @phpstan-return T|false
- */
- public function prev()
- {
- // FIXME: this method does not rewind the internal pointer!
- $key = (string)$this->key();
- $prev = $this->prevSibling($key);
- return $prev !== $this->current() ? $prev : false;
- }
- /**
- * Return nth item.
- * @param int $key
- * @return PageInterface|bool
- * @phpstan-return T|false
- */
- public function nth($key)
- {
- return $this->slice($key, 1)[0] ?? false;
- }
- /**
- * Pick one or more random entries.
- *
- * @param int $num Specifies how many entries should be picked.
- * @return static
- * @phpstan-return static<T>
- */
- public function random($num = 1)
- {
- return $this->createFrom($this->shuffle()->slice(0, $num));
- }
- /**
- * Append new elements to the list.
- *
- * @param array $items Items to be appended. Existing keys will be overridden with the new values.
- * @return static
- * @phpstan-return static<T>
- */
- public function append($items)
- {
- throw new RuntimeException(__METHOD__ . '(): Not Implemented');
- }
- /**
- * Split collection into array of smaller collections.
- *
- * @param int $size
- * @return static[]
- * @phpstan-return static<T>[]
- */
- public function batch($size): array
- {
- $chunks = $this->chunk($size);
- $list = [];
- foreach ($chunks as $chunk) {
- $list[] = $this->createFrom($chunk);
- }
- return $list;
- }
- /**
- * Reorder collection.
- *
- * @param string $by
- * @param string $dir
- * @param array|null $manual
- * @param int|null $sort_flags
- * @return static
- * @phpstan-return static<T>
- */
- public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
- {
- if (!$this->count()) {
- return $this;
- }
- if ($by === 'random') {
- return $this->shuffle();
- }
- $keys = $this->buildSort($by, $dir, $manual, $sort_flags);
- return $this->createFrom(array_replace(array_flip($keys), $this->toArray()) ?? []);
- }
- /**
- * @param string $order_by
- * @param string $order_dir
- * @param array|null $manual
- * @param int|null $sort_flags
- * @return array
- */
- protected function buildSort($order_by = 'default', $order_dir = 'asc', $manual = null, $sort_flags = null): array
- {
- // do this header query work only once
- $header_query = null;
- $header_default = null;
- if (strpos($order_by, 'header.') === 0) {
- $query = explode('|', str_replace('header.', '', $order_by), 2);
- $header_query = array_shift($query) ?? '';
- $header_default = array_shift($query);
- }
- $list = [];
- foreach ($this as $key => $child) {
- switch ($order_by) {
- case 'title':
- $list[$key] = $child->title();
- break;
- case 'date':
- $list[$key] = $child->date();
- $sort_flags = SORT_REGULAR;
- break;
- case 'modified':
- $list[$key] = $child->modified();
- $sort_flags = SORT_REGULAR;
- break;
- case 'publish_date':
- $list[$key] = $child->publishDate();
- $sort_flags = SORT_REGULAR;
- break;
- case 'unpublish_date':
- $list[$key] = $child->unpublishDate();
- $sort_flags = SORT_REGULAR;
- break;
- case 'slug':
- $list[$key] = $child->slug();
- break;
- case 'basename':
- $list[$key] = Utils::basename($key);
- break;
- case 'folder':
- $list[$key] = $child->folder();
- break;
- case 'manual':
- case 'default':
- default:
- if (is_string($header_query)) {
- /** @var Header $child_header */
- $child_header = $child->header();
- $header_value = $child_header->get($header_query);
- if (is_array($header_value)) {
- $list[$key] = implode(',', $header_value);
- } elseif ($header_value) {
- $list[$key] = $header_value;
- } else {
- $list[$key] = $header_default ?: $key;
- }
- $sort_flags = $sort_flags ?: SORT_REGULAR;
- break;
- }
- $list[$key] = $key;
- $sort_flags = $sort_flags ?: SORT_REGULAR;
- }
- }
- if (null === $sort_flags) {
- $sort_flags = SORT_NATURAL | SORT_FLAG_CASE;
- }
- // else just sort the list according to specified key
- if (extension_loaded('intl') && Grav::instance()['config']->get('system.intl_enabled')) {
- $locale = setlocale(LC_COLLATE, '0'); //`setlocale` with a '0' param returns the current locale set
- $col = Collator::create($locale);
- if ($col) {
- $col->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
- if (($sort_flags & SORT_NATURAL) === SORT_NATURAL) {
- $list = preg_replace_callback('~([0-9]+)\.~', static function ($number) {
- return sprintf('%032d.', $number[0]);
- }, $list);
- if (!is_array($list)) {
- throw new RuntimeException('Internal Error');
- }
- $list_vals = array_values($list);
- if (is_numeric(array_shift($list_vals))) {
- $sort_flags = Collator::SORT_REGULAR;
- } else {
- $sort_flags = Collator::SORT_STRING;
- }
- }
- $col->asort($list, $sort_flags);
- } else {
- asort($list, $sort_flags);
- }
- } else {
- asort($list, $sort_flags);
- }
- // Move manually ordered items into the beginning of the list. Order of the unlisted items does not change.
- if (is_array($manual) && !empty($manual)) {
- $i = count($manual);
- $new_list = [];
- foreach ($list as $key => $dummy) {
- $child = $this[$key] ?? null;
- $order = $child ? array_search($child->slug, $manual, true) : false;
- if ($order === false) {
- $order = $i++;
- }
- $new_list[$key] = (int)$order;
- }
- $list = $new_list;
- // Apply manual ordering to the list.
- asort($list, SORT_NUMERIC);
- }
- if ($order_dir !== 'asc') {
- $list = array_reverse($list);
- }
- return array_keys($list);
- }
- /**
- * Mimicks Pages class.
- *
- * @return $this
- * @deprecated 1.7 Not needed anymore in Flex Pages (does nothing).
- */
- public function all()
- {
- return $this;
- }
- /**
- * Returns the items between a set of date ranges of either the page date field (default) or
- * an arbitrary datetime page field where start date and end date are optional
- * Dates must be passed in as text that strtotime() can process
- * http://php.net/manual/en/function.strtotime.php
- *
- * @param string|null $startDate
- * @param string|null $endDate
- * @param string|null $field
- * @return static
- * @phpstan-return static<T>
- * @throws Exception
- */
- public function dateRange($startDate = null, $endDate = null, $field = null)
- {
- $start = $startDate ? Utils::date2timestamp($startDate) : null;
- $end = $endDate ? Utils::date2timestamp($endDate) : null;
- $entries = [];
- foreach ($this as $key => $object) {
- if (!$object) {
- continue;
- }
- $date = $field ? strtotime($object->getNestedProperty($field)) : $object->date();
- if ((!$start || $date >= $start) && (!$end || $date <= $end)) {
- $entries[$key] = $object;
- }
- }
- return $this->createFrom($entries);
- }
- /**
- * Creates new collection with only visible pages
- *
- * @return static The collection with only visible pages
- * @phpstan-return static<T>
- */
- public function visible()
- {
- $entries = [];
- foreach ($this as $key => $object) {
- if ($object && $object->visible()) {
- $entries[$key] = $object;
- }
- }
- return $this->createFrom($entries);
- }
- /**
- * Creates new collection with only non-visible pages
- *
- * @return static The collection with only non-visible pages
- * @phpstan-return static<T>
- */
- public function nonVisible()
- {
- $entries = [];
- foreach ($this as $key => $object) {
- if ($object && !$object->visible()) {
- $entries[$key] = $object;
- }
- }
- return $this->createFrom($entries);
- }
- /**
- * Creates new collection with only pages
- *
- * @return static The collection with only pages
- * @phpstan-return static<T>
- */
- public function pages()
- {
- $entries = [];
- /**
- * @var int|string $key
- * @var PageInterface|null $object
- */
- foreach ($this as $key => $object) {
- if ($object && !$object->isModule()) {
- $entries[$key] = $object;
- }
- }
- return $this->createFrom($entries);
- }
- /**
- * Creates new collection with only modules
- *
- * @return static The collection with only modules
- * @phpstan-return static<T>
- */
- public function modules()
- {
- $entries = [];
- /**
- * @var int|string $key
- * @var PageInterface|null $object
- */
- foreach ($this as $key => $object) {
- if ($object && $object->isModule()) {
- $entries[$key] = $object;
- }
- }
- return $this->createFrom($entries);
- }
- /**
- * Alias of modules()
- *
- * @return static
- * @phpstan-return static<T>
- */
- public function modular()
- {
- return $this->modules();
- }
- /**
- * Alias of pages()
- *
- * @return static
- * @phpstan-return static<T>
- */
- public function nonModular()
- {
- return $this->pages();
- }
- /**
- * Creates new collection with only published pages
- *
- * @return static The collection with only published pages
- * @phpstan-return static<T>
- */
- public function published()
- {
- $entries = [];
- foreach ($this as $key => $object) {
- if ($object && $object->published()) {
- $entries[$key] = $object;
- }
- }
- return $this->createFrom($entries);
- }
- /**
- * Creates new collection with only non-published pages
- *
- * @return static The collection with only non-published pages
- * @phpstan-return static<T>
- */
- public function nonPublished()
- {
- $entries = [];
- foreach ($this as $key => $object) {
- if ($object && !$object->published()) {
- $entries[$key] = $object;
- }
- }
- return $this->createFrom($entries);
- }
- /**
- * Creates new collection with only routable pages
- *
- * @return static The collection with only routable pages
- * @phpstan-return static<T>
- */
- public function routable()
- {
- $entries = [];
- foreach ($this as $key => $object) {
- if ($object && $object->routable()) {
- $entries[$key] = $object;
- }
- }
- return $this->createFrom($entries);
- }
- /**
- * Creates new collection with only non-routable pages
- *
- * @return static The collection with only non-routable pages
- * @phpstan-return static<T>
- */
- public function nonRoutable()
- {
- $entries = [];
- foreach ($this as $key => $object) {
- if ($object && !$object->routable()) {
- $entries[$key] = $object;
- }
- }
- return $this->createFrom($entries);
- }
- /**
- * Creates new collection with only pages of the specified type
- *
- * @param string $type
- * @return static The collection
- * @phpstan-return static<T>
- */
- public function ofType($type)
- {
- $entries = [];
- foreach ($this as $key => $object) {
- if ($object && $object->template() === $type) {
- $entries[$key] = $object;
- }
- }
- return $this->createFrom($entries);
- }
- /**
- * Creates new collection with only pages of one of the specified types
- *
- * @param string[] $types
- * @return static The collection
- * @phpstan-return static<T>
- */
- public function ofOneOfTheseTypes($types)
- {
- $entries = [];
- foreach ($this as $key => $object) {
- if ($object && in_array($object->template(), $types, true)) {
- $entries[$key] = $object;
- }
- }
- return $this->createFrom($entries);
- }
- /**
- * Creates new collection with only pages of one of the specified access levels
- *
- * @param array $accessLevels
- * @return static The collection
- * @phpstan-return static<T>
- */
- public function ofOneOfTheseAccessLevels($accessLevels)
- {
- $entries = [];
- foreach ($this as $key => $object) {
- if ($object && isset($object->header()->access)) {
- if (is_array($object->header()->access)) {
- //Multiple values for access
- $valid = false;
- foreach ($object->header()->access as $index => $accessLevel) {
- if (is_array($accessLevel)) {
- foreach ($accessLevel as $innerIndex => $innerAccessLevel) {
- if (in_array($innerAccessLevel, $accessLevels)) {
- $valid = true;
- }
- }
- } else {
- if (in_array($index, $accessLevels)) {
- $valid = true;
- }
- }
- }
- if ($valid) {
- $entries[$key] = $object;
- }
- } else {
- //Single value for access
- if (in_array($object->header()->access, $accessLevels)) {
- $entries[$key] = $object;
- }
- }
- }
- }
- return $this->createFrom($entries);
- }
- /**
- * @param bool $bool
- * @return static
- * @phpstan-return static<T>
- */
- public function withOrdered(bool $bool = true)
- {
- $list = array_keys(array_filter($this->call('isOrdered', [$bool])));
- return $this->select($list);
- }
- /**
- * @param bool $bool
- * @return static
- * @phpstan-return static<T>
- */
- public function withModules(bool $bool = true)
- {
- $list = array_keys(array_filter($this->call('isModule', [$bool])));
- return $this->select($list);
- }
- /**
- * @param bool $bool
- * @return static
- * @phpstan-return static<T>
- */
- public function withPages(bool $bool = true)
- {
- $list = array_keys(array_filter($this->call('isPage', [$bool])));
- return $this->select($list);
- }
- /**
- * @param bool $bool
- * @param string|null $languageCode
- * @param bool|null $fallback
- * @return static
- * @phpstan-return static<T>
- */
- public function withTranslation(bool $bool = true, string $languageCode = null, bool $fallback = null)
- {
- $list = array_keys(array_filter($this->call('hasTranslation', [$languageCode, $fallback])));
- return $bool ? $this->select($list) : $this->unselect($list);
- }
- /**
- * @param string|null $languageCode
- * @param bool|null $fallback
- * @return PageIndex
- */
- public function withTranslated(string $languageCode = null, bool $fallback = null)
- {
- return $this->getIndex()->withTranslated($languageCode, $fallback);
- }
- /**
- * Filter pages by given filters.
- *
- * - search: string
- * - page_type: string|string[]
- * - modular: bool
- * - visible: bool
- * - routable: bool
- * - published: bool
- * - page: bool
- * - translated: bool
- *
- * @param array $filters
- * @param bool $recursive
- * @return static
- * @phpstan-return static<T>
- */
- public function filterBy(array $filters, bool $recursive = false)
- {
- $list = array_keys(array_filter($this->call('filterBy', [$filters, $recursive])));
- return $this->select($list);
- }
- /**
- * Get the extended version of this Collection with each page keyed by route
- *
- * @return array
- * @throws Exception
- */
- public function toExtendedArray(): array
- {
- $entries = [];
- foreach ($this as $key => $object) {
- if ($object) {
- $entries[$object->route()] = $object->toArray();
- }
- }
- return $entries;
- }
- /**
- * @param array $options
- * @return array
- */
- public function getLevelListing(array $options): array
- {
- /** @var PageIndex $index */
- $index = $this->getIndex();
- return method_exists($index, 'getLevelListing') ? $index->getLevelListing($options) : [];
- }
- }
|