723 lines
19 KiB
PHP
723 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 mehod 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 is_object($item) && $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]; /** @var Page $page */
|
|
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 $pages Pages to import.
|
|
* @return PageArray reference to current instance.
|
|
*
|
|
*/
|
|
public function import($pages) {
|
|
if(is_object($pages) && $pages instanceof Page) $pages = array($pages);
|
|
if(!self::iterable($pages)) return $this;
|
|
foreach($pages as $page) $this->add($page);
|
|
if($pages instanceof PageArray) {
|
|
if(count($pages) < $pages->getTotal()) {
|
|
$this->setTotal($this->getTotal() + ($pages->getTotal() - count($pages)));
|
|
}
|
|
}
|
|
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(is_object($key) && $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 $page 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($page) {
|
|
|
|
if($this->isValidItem($page)) {
|
|
parent::add($page);
|
|
|
|
} else if($page instanceof PageArray || is_array($page)) {
|
|
return $this->import($page);
|
|
|
|
} else if(ctype_digit("$page")) {
|
|
$page = $this->wire()->pages->get("id=$page");
|
|
if($page->id) parent::add($page);
|
|
}
|
|
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) {
|
|
return parent::findRandom($num);
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
return parent::slice($start, $limit);
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
return parent::eq($num);
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
return parent::find($selector);
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
return parent::findOne($selector);
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
$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
|
|
*
|
|
*/
|
|
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 $key => $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;
|
|
/** @var Page $item */
|
|
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;
|
|
/** @var Page $item */
|
|
if(isset($this->keyIndex[$item->id])) {
|
|
if($this->numTotal) $this->numTotal--;
|
|
unset($this->keyIndex[$item->id]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|