artabro/wire/core/PageArray.php
2024-08-27 11:35:37 +02:00

733 lines
19 KiB
PHP

<?php namespace ProcessWire;
/**
* ProcessWire PageArray
*
* PageArray provides an array-like means for storing PageReferences and is utilized throughout ProcessWire.
*
* #pw-summary PageArray is a paginated type of WireArray that holds multiple Page objects.
* #pw-body =
* **Please see the `WireArray` and `PaginatedArray` types for available methods**, as they are not
* repeated here, except where PageArray has modified or extended those types in some manner.
* The PageArray type is functionally identical to WireArray and PaginatedArray except that it is focused
* specifically on managing Page objects.
*
* PageArray is returned by all API methods in ProcessWire that can return more than one page at once.
* `$pages->find()` and `$page->children()` are common examples that return PageArray.
*
* You can create a new PageArray using any of the methods below:
* ~~~~~
* // the most common way to create a new PageArray and add a $page to it
* $a = new PageArray();
* $a->add($page);
*
* // ProcessWire 3.0.123+ can also create PageArray like this:
* $a = PageArray(); // create blank
* $a = PageArray($page); // create + add one page
* $a = PageArray([ $page1, $page2, $page3 ]); // create + add pages
* ~~~~~
* #pw-body
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
* @method string getMarkup($key = null) Render a simple/default markup value for each item #pw-internal
* @property Page|null $first First item
* @property Page|null $last Last item
* @property Page[] $data #pw-internal
*
*/
class PageArray extends PaginatedArray implements WirePaginatable {
/**
* Reference to the selectors that led to this PageArray, if applicable
*
* @var Selectors|string
*
*/
protected $selectors = null;
/**
* Options that were passed to $pages->find() that led to this PageArray, when applicable.
*
* Applies only for lazy loading result sets.
*
* @var array
*
*/
protected $finderOptions = array();
/**
* Is this a lazy-loaded PageArray?
*
* @var bool
*
*/
protected $lazyLoad = false;
/**
* Index of item keys of page_id => data key
*
* @var array
*
*/
protected $keyIndex = array();
/**
* Template method that descendant classes may use to validate items added to this WireArray
*
* #pw-internal
*
* @param mixed $item Item to add
* @return bool True if item is valid and may be added, false if not
*
*/
public function isValidItem($item) {
return $item instanceof Page;
}
/**
* Validate the key used to add a Page
*
* PageArrays are keyed by an incremental number that does NOT relate to the Page ID.
*
* #pw-internal
*
* @param string|int $key
* @return bool True if key is valid and may be used, false if not
*
*/
public function isValidKey($key) {
return ctype_digit("$key");
}
/**
* Get the array key for the given Page item
*
* This method is used internally by the add() and prepend() methods.
*
* #pw-internal
*
* @param Page $item Page to get key for
* @return string|int|null Found key, or null if not found.
*
*/
public function getItemKey($item) {
if(!$item instanceof Page) return null;
if(!$this->duplicateChecking) return parent::getItemKey($item);
// first see if we can determine key from our index
$id = $item->id;
if(isset($this->keyIndex[$id])) {
// given item exists in this PageArray (or at least has)
$key = $this->keyIndex[$id];
if(isset($this->data[$key])) {
$page = $this->data[$key];
if($page->id === $id) {
// found it
return $key;
}
}
// if this (maybe unreachable) point is reached, then index needs to
// be rebuilt because either item is no longer here, or has moved
$this->keyIndex = array();
foreach($this->data as $key => $page) {
$this->keyIndex[$page->id] = $key;
}
return isset($this->keyIndex[$id]) ? $this->keyIndex[$id] : null;
} else {
// page is not present here
return null;
}
}
/**
* Does this PageArray use numeric keys only? (yes it does)
*
* Defined here to override the slower check in WireArray
*
* @return bool
*
*/
protected function usesNumericKeys() {
return true;
}
/**
* Per WireArray interface, return a blank Page
*
* #pw-internal
*
* @return Page
*
*/
public function makeBlankItem() {
return $this->wire()->pages->newPage();
}
/**
* Creates a new blank instance of this PageArray, for internal use.
*
* #pw-internal
*
* @return PageArray
*
*/
public function makeNew() {
$class = get_class($this);
/** @var PageArray $newArray */
$newArray = $this->wire(new $class());
// $newArray->finderOptions($this->finderOptions());
if($this->lazyLoad) $newArray->_lazy(true);
return $newArray;
}
/**
* Import the provided pages into this PageArray.
*
* #pw-internal
*
* @param array|PageArray|Page $items Pages to import.
* @return PageArray reference to current instance.
*
*/
public function import($items) {
if($items instanceof Page) $items = array($items);
if(!self::iterable($items)) return $this;
foreach($items as $page) $this->add($page);
if($items instanceof PageArray) {
if(count($items) < $items->getTotal()) {
$this->setTotal($this->getTotal() + ($items->getTotal() - count($items)));
}
}
return $this;
}
/**
* Does this PageArray contain the given index or Page?
*
* #pw-internal
*
* @param Page|int $key Page Array index or Page object.
* @return bool True if the index or Page exists here, false if not.
*/
public function has($key) {
if($key instanceof Page) {
return $this->getItemKey($key) !== null;
}
return parent::has($key);
}
/**
* Add one or more Page objects to this PageArray.
*
* Please see the `WireArray::add()` method for more details.
*
* ~~~~~
* // Add one page
* $pageArray->add($page);
*
* // Add multiple pages
* $pageArray->add($pages->find("template=basic-page"));
*
* // Add page by ID
* $pageArray->add(1005);
* ~~~~~
*
* @param Page|PageArray|int $item Page object, PageArray object, or Page ID.
* - If given a `Page`, the Page will be added.
* - If given a `PageArray`, it will do the same thing as the `WireArray::import()` method and append all the pages.
* - If Page `ID`, the Page identified by that ID will be loaded and added to the PageArray.
* @return $this
*/
public function add($item) {
if($this->isValidItem($item)) {
parent::add($item);
} else if($item instanceof PageArray || is_array($item)) {
return $this->import($item);
} else if(ctype_digit("$item")) {
$item = $this->wire()->pages->get("id=$item");
if($item->id) parent::add($item);
}
return $this;
}
/**
* Get one or more random pages from this PageArray.
*
* If one item is requested, the item is returned (unless $alwaysArray is true).
* If multiple items are requested, a new WireArray of those items is returned.
*
* #pw-internal
*
* @param int $num Number of items to return. Optional and defaults to 1.
* @param bool $alwaysArray If true, then method will always return a container of items, even if it only contains 1.
* @return Page|PageArray Returns value of item, or new PageArray of items if more than one requested.
*/
public function getRandom($num = 1, $alwaysArray = false) {
return parent::getRandom($num, $alwaysArray);
}
/**
* Get a quantity of random pages from this PageArray.
*
* Unlike getRandom() this one always returns a PageArray (or derived type).
*
* #pw-internal
*
* @param int $num Number of items to return
* @return PageArray|WireArray New PageArray instance
*
*/
public function findRandom($num) {
/** @var PageArray $value */
$value = parent::findRandom($num);
return $value;
}
/**
* Get a slice of the PageArray.
*
* Given a starting point and a number of items, returns a new PageArray of those items.
* If $limit is omitted, then it includes everything beyond the starting point.
*
* #pw-internal
*
* @param int $start Starting index.
* @param int $limit Number of items to include. If omitted, includes the rest of the array.
* @return PageArray|WireArray New PageArray instance
*
*/
public function slice($start, $limit = 0) {
/** @var PageArray $value */
$value = parent::slice($start, $limit);
return $value;
}
/**
* Returns the item at the given index starting from 0, or NULL if it doesn't exist.
*
* Unlike the index() method, this returns an actual item and not another PageArray.
*
* #pw-internal
*
* @param int $num Return the nth item in this WireArray. Specify a negative number to count from the end rather than the start.
* @return Page|Wire|null Returns Page object or null if not present
*
*/
public function eq($num) {
/** @var Page $value */
$value = parent::eq($num);
return $value;
}
/**
* Returns the first item in the PageArray or boolean FALSE if empty.
*
* #pw-internal
*
* @return Page|bool
*
*/
public function first() {
return parent::first();
}
/**
* Returns the last item in the PageArray or boolean FALSE if empty.
*
* #pw-internal
*
* @return Page|bool
*
*/
public function last() {
return parent::last();
}
/**
* Set the Selectors that led to this PageArray, if applicable
*
* #pw-internal
*
* @param Selectors|string $selectors Option to add as string added in 3.0.142
* @return $this
*
*/
public function setSelectors($selectors) {
if(is_string($selectors) || $selectors instanceof Selectors || $selectors === null) {
$this->selectors = $selectors;
}
return $this;
}
/**
* Return the Selectors that led to this PageArray, or null if not set/applicable.
*
* Use this to retrieve the Selectors that were used to find this group of pages,
* if dealing with a PageArray that originated from a database operation.
*
* ~~~~~
* $products = $pages->find("template=product, featured=1, sort=-modified, limit=10");
* echo $products->getSelectors(); // outputs the selector above
* ~~~~~
*
* @param bool $getString Specify true to get selector string rather than Selectors object (default=false) added in 3.0.142
* @return Selectors|string|null Returns Selectors object if available, or null if not. Always return string if $getString argument is true.
*
*/
public function getSelectors($getString = false) {
if($getString) return (string) $this->selectors;
if($this->selectors === null) return null;
if(is_string($this->selectors)) $this->selectors = $this->wire(new Selectors($this->selectors));
return $this->selectors;
}
/**
* Filter out Pages that don't match the selector.
*
* This is applicable to and destructive to the WireArray.
*
* @param string|Selectors|array $selectors AttributeSelector string to use as the filter.
* @param bool|int $not Make this a "not" filter? Use int 1 for "not all". (default is false)
* @return PageArray|WireArray reference to current [filtered] PageArray
*
*/
protected function filterData($selectors, $not = false) {
if(is_string($selectors) && $selectors[0] === '/') $selectors = "path=$selectors";
return parent::filterData($selectors, $not);
}
/**
* Filter out pages that don't match the selector (destructive)
*
* #pw-internal
*
* @param string $selector AttributeSelector string to use as the filter.
* @return PageArray|PaginatedArray|WireArray reference to current PageArray instance.
*
*/
public function filter($selector) {
return parent::filter($selector);
}
/**
* Filter out pages that don't match the selector (destructive)
*
* #pw-internal
*
* @param string $selector AttributeSelector string to use as the filter.
* @return PageArray|PaginatedArray|WireArray reference to current PageArray instance.
*
*/
public function not($selector) {
return parent::not($selector);
}
/**
* Like the base get() method but can only return Page objects (whether Page or NullPage)
*
* @param int|string|array $key Provide any of the following:
* - Key of Page to retrieve.
* - A selector string or selector array, to return the first item that matches the selector.
* - A string containing the "name" property of any Page, and the matching Page will be returned.
* @return Page|NullPage
* @since 3.0.162
* @see WireArray::get()
*
*/
public function getPage($key) {
$value = $this->get($key);
return $value instanceof Page ? $value : $this->wire()->pages->newNullPage();
}
/**
* Find all pages in this PageArray that match the given selector (non-destructive)
*
* This is non destructive and returns a brand new PageArray.
*
* #pw-internal
*
* @param string $selector AttributeSelector string.
* @return PageArray|WireArray New PageArray instance
* @see WireArray::find()
*
*/
public function find($selector) {
/** @var PageArray $value */
$value = parent::find($selector);
return $value;
}
/**
* Same as find() method, but returns a single Page rather than PageArray or FALSE if empty.
*
* #pw-internal
*
* @param string $selector
* @return Page|bool
* @see WireArray::findOne()
*
*/
public function findOne($selector) {
/** @var Page|bool $value */
$value = parent::findOne($selector);
return $value;
}
/**
* Same as find() or findOne() methods, but always returns a Page (whether Page or NullPage)
*
* @param string $selector
* @return Page|NullPage
* @since 3.0.162
*
*/
public function findOnePage($selector) {
$value = parent::findOne($selector);
return $value instanceof Page ? $value : $this->wire()->pages->newNullPage();
}
/**
* Get Page from this PageArray having given name, or return NullPage if not present
*
* @param string $name
* @return NullPage|Page
* @since 3.0.162
*
*/
public function getPageByName($name) {
return $this->getPageByProperty('name', $name, true);
}
/**
* Get Page from this PageArray having given ID, or return NullPage if not present
*
* @param int $id
* @return NullPage|Page
* @since 3.0.162
*
*/
public function getPageByID($id) {
$id = (int) $id;
if(isset($this->keyIndex[$id])) {
$k = $this->keyIndex[$id];
if(isset($this->data[$k]) && $this->data[$k]->id === $id) return $this->data[$k];
}
return $this->getPageByProperty('id', (int) $id, true);
}
/**
* Get first found Page object matching property/value, or return NullPage if not present in this PageArray
*
* #pw-internal
*
* @param string $property Name of page property or field
* @param string|mixed $value Value to match
* @param bool $strict Match value with strict type enforcement? (default=false)
* @return Page|NullPage
* @since 3.0.162
*
*/
public function getPageByProperty($property, $value, $strict = false) {
$foundPage = null;
foreach($this->data as $item) {
if($strict) {
if($item->get($property) === $value) $foundPage = $item;
} else {
if($item->get($property) == $value) $foundPage = $item;
}
if($foundPage) break;
}
return $foundPage ? $foundPage : $this->wire()->pages->newNullPage();
}
/**
* Prepare selectors for filtering
*
* Template method for descending classes to modify selectors if needed
*
* @param Selectors $selectors
*
*/
protected function filterDataSelectors(Selectors $selectors) {
$disallowed = array('include', 'check_access', 'checkAccess');
foreach($selectors as $selector) {
if(in_array($selector->field(), $disallowed)) {
$selectors->remove($selector);
}
}
parent::filterDataSelectors($selectors);
}
/**
* Get the value of $property from $item
*
* Used by the WireArray::sort method to retrieve a value from a Wire object.
* If output formatting is on, we turn it off to ensure that the sorting
* is performed without output formatting.
*
* @param Wire $item
* @param string $property
* @return mixed
*
*/
protected function getItemPropertyValue(Wire $item, $property) {
if($item instanceof Page) {
$value = $item->getUnformatted($property);
} else if(strpos($property, '.') !== false) {
$value = WireData::_getDot($property, $item);
} else if($item instanceof WireArray) {
/** @var PageArray $item */
$value = $item->getProperty($property);
if(is_null($value)) {
$value = $item->first();
if($value) $value = $this->getItemPropertyValue($value, $property);
}
} else {
$value = $item->$property;
}
if(is_array($value)) $value = implode('|', $value);
return $value;
}
/**
* Allows iteration of the PageArray.
*
* #pw-internal
*
* @return Page[]|\ArrayObject|PageArrayIterator
*
*/
#[\ReturnTypeWillChange]
public function getIterator() {
if($this->lazyLoad) return new PageArrayIterator($this->data, $this->finderOptions);
return parent::getIterator();
}
/**
* PageArrays always return a string of the Page IDs separated by pipe "|" characters
*
* Pipe charactesr are used for compatibility with Selector OR statements
*
*/
public function __toString() {
$s = '';
foreach($this as $page) $s .= "$page|";
$s = rtrim($s, "|");
return $s;
}
/**
* Render a simple/default markup value for each item in this PageArray.
*
* For testing/debugging purposes.
*
* #pw-internal
*
* @param string|callable $key
* @return string
*
*/
public function ___getMarkup($key = null) {
if($key && !is_string($key)) {
$out = $this->each($key);
} else if(strpos($key, '{') !== false && strpos($key, '}')) {
$out = $this->each($key);
} else {
if(empty($key)) $key = "<li>{title|name}</li>";
$out = $this->each($key);
if($out) {
$out = "<ul>$out</ul>";
if($this->getLimit() && $this->getTotal() > $this->getLimit()) {
$pager = $this->wire('modules')->get('MarkupPagerNav');
$out .= $pager->render($this);
}
}
}
return $out;
}
/**
* debugInfo PHP 5.6+ magic method
*
* @return array
*
*/
public function __debugInfo() {
$info = parent::__debugInfo();
$info['selectors'] = (string) $this->selectors;
if(!wireCount($info['selectors'])) unset($info['selectors']);
return $info;
}
/**
* Get or set $options array used by $pages->find() for this PageArray
*
* #pw-internal
*
* @param array|null $options Specify array to set or omit this argument to get
* @return array
*
*/
public function finderOptions($options = null) {
if(is_array($options)) $this->finderOptions = $options;
return $this->finderOptions;
}
/**
* Get or set Lazy loading state of this PageArray
*
* #pw-internal
*
* @param bool|null $lazy
* @return bool
*
*/
public function _lazy($lazy = null) {
if(is_bool($lazy)) $this->lazyLoad = $lazy;
return $this->lazyLoad;
}
/**
* Track an item added
*
* @param Wire|mixed $item
* @param int|string $key
*
*/
protected function trackAdd($item, $key) {
parent::trackAdd($item, $key);
if(!$item instanceof Page) return;
if(!isset($this->keyIndex[$item->id])) $this->numTotal++;
$this->keyIndex[$item->id] = $key;
}
/**
* Track an item removed
*
* @param Wire|mixed $item
* @param int|string $key
*
*/
protected function trackRemove($item, $key) {
parent::trackRemove($item, $key);
if(!$item instanceof Page) return;
if(isset($this->keyIndex[$item->id])) {
if($this->numTotal) $this->numTotal--;
unset($this->keyIndex[$item->id]);
}
}
}