Rev 1 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ProcessWire WireArray** WireArray is the base array access object used in the ProcessWire framework.** Several methods are duplicated here for syntactical convenience and jQuery-like usability.* Many methods act upon the array and return $this, which enables WireArrays to be used for fluent interfaces.* WireArray is the base of the PageArray (subclass) which is the most used instance.** @todo can we implement next() and prev() like on Page, as alias to getNext() and getPrev()?** ProcessWire 3.x, Copyright 2021 by Ryan Cramer* https://processwire.com** @method WireArray and($item)* @method static WireArray new($items = array())* @property int $count Number of items* @property Wire|null $first First item* @property Wire|null $last Last item* @property array $keys All keys used in this WireArray* @property array $values All values used in this WireArray** #pw-order-groups traversal,retrieval,manipulation,info,output-rendering,other-data-storage,changes,fun-tools,hooker* #pw-summary WireArray is the base iterable array type used throughout the ProcessWire framework.** #pw-body =* **Nearly all collections of items in ProcessWire are derived from the WireArray type.*** This includes collections of pages, fields, templates, modules and more. As a result, the WireArray class is one* you will be interacting with regularly in the ProcessWire API, whether you know it or not.** Below are all the public methods you can use to interact with WireArray types in ProcessWire. In addition to these* methods, you can also treat WireArray types like regular PHP arrays, in that you can `foreach()` them and get or* set elements using array syntax, i.e. `$value = $items[$key];` to get an item or `$items[] = $item;` to add an item.* #pw-body**/class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Countable {/*** Basic type managed by the WireArray for data storage** @var Wire[]**/protected $data = array();/*** Any extra user defined data to accompany the WireArray** See the data() method. Note these are not under change tracking.**/protected $extraData = array();/*** Array containing the items that have been removed from this WireArray while trackChanges is on** @see getRemovedKeys()**/protected $itemsRemoved = array();/*** Array containing the items that have been added to this WireArray while trackChanges is on** @see getRemovedKeys()**/protected $itemsAdded = array();/*** Prevent addition of duplicates?** Applies only to non-associative WireArray types.** @var bool**/protected $duplicateChecking = true;/*** Flags for PHP sort functions** @var int**/protected $sortFlags = 0; // 0 == SORT_REGULAR/*** Construct**/public function __construct() {if($this->className() === 'WireArray') $this->duplicateChecking = false;}/*** Is the given item valid for storange in this array?** Template method that descending classes may use to validate items added to this WireArray** #pw-group-info** @param mixed $item Item to test for validity* @return bool True if item is valid and may be added, false if not**/public function isValidItem($item) {if($item instanceof Wire) return true;$className = $this->className();if($className === 'WireArray' || $className === 'PaginatedArray') return true;return false;}/*** Is the given item key valid for use in this array?** Template method that descendant classes may use to validate the key of items added to this WireArray** #pw-group-info** @param string|int $key Key to test* @return bool True if key is valid and may be used, false if not**/public function isValidKey($key) {// unused $key intentional for descending class/template purposesif($key) {}return true;}/*** Is the given WireArray identical to this one?** #pw-group-info** @param WireArray $items* @param bool|int $strict Use strict mode? Optionally specify one of the following:* `true` (boolean): Default. Compares items, item object instances, order, and any other data contained in WireArray.* `false` (boolean): Compares only that items in the WireArray resolve to the same order and values (though not object instances).* @return bool True if identical, false if not.**/public function isIdentical(WireArray $items, $strict = true) {if($items === $this) return true;if($items->className() != $this->className()) return false;if(!$strict) return ((string) $this) === ((string) $items);$a1 = $this->getArray();$a2 = $items->getArray();if($a1 === $a2) {// all items match$d1 = $this->data();$d2 = $items->data();if($d1 === $d2) {// all data matchesreturn true;}}return false;}/*** Get the array key for the given item** - This is a template method that descendant classes may use to find a key from the item itself, or null if disabled.* - This method is used internally by the add() and prepend() methods.** #pw-internal** @param object|Wire $item Item to get key for* @return string|int|null Found key, or null if not found.**/public function getItemKey($item) {// in this base class, we don't make assumptions how the key is determined// so we just search the array to see if the item is already here and// return it's key if it is here$key = array_search($item, $this->data, true);return $key === false ? null : $key;}/*** Get a new/blank item of the type that this WireArray holds** #pw-internal** @throws WireException If class doesn't implement this method.* @return Wire|null**/public function makeBlankItem() {$class = wireClassName($this, false);if($class != 'WireArray' && $class != 'PaginatedArray') {throw new WireException("Class '$class' doesn't yet implement method 'makeBlankItem()' and it needs to.");}return null;}/*** Creates a new blank instance of this WireArray, for internal use.** #pw-internal** @return WireArray**/public function makeNew() {$class = get_class($this);$newArray = $this->wire(new $class());return $newArray;}/*** Creates a new populated copy/clone of this WireArray** Same as a clone, except that descending classes may wish to replace the* clone call a manually created WireArray to prevent deep cloning.** #pw-internal** @return WireArray**/public function makeCopy() {return clone $this;}/*** Import the given item(s) into this WireArray.** - Adds imported items to the end of the WireArray.* - Skips over any items already present in the WireArray (when duplicateChecking is enabled)** #pw-group-manipulation** @param array|WireArray $items Items to import.* @return $this* @throws WireException If given items not compatible with the WireArray**/public function import($items) {if(!is_array($items) && !self::iterable($items)) {throw new WireException('WireArray cannot import non arrays or non-iterable objects');}foreach($items as $key => $value) {if($this->duplicateChecking) {if(($k = $this->getItemKey($value)) !== null) $key = $k;if(isset($this->data[$key])) continue; // won't overwrite existing keys$this->set($key, $value);} else {$this->add($value);}}return $this;}/*** Add an item to the end of the WireArray.** ~~~~~* $items->add($item);* ~~~~~** #pw-group-manipulation** @param int|string|array|object|Wire|WireArray $item Item to add.* @return $this* @throws WireException If given an item that can't be stored by this WireArray.* @see WireArray::prepend(), WireArray::append()**/public function add($item) {if(!$this->isValidItem($item)) {if($item instanceof WireArray) {foreach($item as $i) $this->prepend($i);return $this;} else {throw new WireException("Item added to " . get_class($this) . " is not an allowed type");}}$key = null;if($this->duplicateChecking && ($key = $this->getItemKey($item)) !== null) {// avoid two copies of the same item, re-add it to the endif(isset($this->data[$key])) unset($this->data[$key]);$this->data[$key] = $item;} else {$this->data[] = $item;end($this->data);$key = key($this->data);}$this->trackChange("add", null, $item);$this->trackAdd($item, $key);return $this;}/*** Insert an item either before or after another** Provides the implementation for the insertBefore and insertAfter functions** @param int|string|array|object $item Item you want to insert* @param int|string|array|object $existingItem Item already present that you want to insert before/afer* @param bool $insertBefore True if you want to insert before, false if after* @return $this* @throws WireException if given an invalid item**/protected function _insert($item, $existingItem, $insertBefore = true) {if(!$this->isValidItem($item)) throw new WireException("You may not insert this item type");$data = array();$this->add($item); // first add the item, then we'll move it$itemKey = $this->getItemKey($item);foreach($this->data as $key => $value) {if($value === $existingItem) {// found $existingItem, so insert $item and then insert $existingItemif($insertBefore) {$data[$itemKey] = $item;$data[$key] = $value;} else {$data[$key] = $value;$data[$itemKey] = $item;}} else if($value === $item) {// skip over it since the above is doing the insertcontinue;} else {// continue populating existing data$data[$key] = $value;}}$this->data = $data;return $this;}/*** Insert an item before an existing item** ~~~~~* $items->insertBefore($newItem, $existingItem);* ~~~~~** #pw-group-manipulation** @param Wire|string|int $item Item you want to insert.* @param Wire|string|int $existingItem Item already present that you want to insert before.* @return $this**/public function insertBefore($item, $existingItem) {return $this->_insert($item, $existingItem, true);}/*** Insert an item after an existing item** ~~~~~* $items->insertAfter($newItem, $existingItem);* ~~~~~** #pw-group-manipulation** @param Wire|string|int $item Item you want to insert* @param Wire|string|int $existingItem Item already present that you want to insert after* @return $this**/public function insertAfter($item, $existingItem) {return $this->_insert($item, $existingItem, false);}/*** Replace one item with the other** - The order of the arguments does not matter.* - If both items are already present, they will change places.* - If one item is not already present, it will replace the one that is.* - If neither item is present, both will be added at the end.** ~~~~~* $items->replace($existingItem, $newItem);* ~~~~~** #pw-group-manipulation** @param Wire|string|int $itemA* @param Wire|string|int $itemB* @return $this**/public function replace($itemA, $itemB) {$a = $this->get($itemA);$b = $this->get($itemB);if($a && $b) {// swap a and b, both already present in this WireArray$data = $this->data;foreach($data as $key => $value) {$k = null;if($value === $a) {if(method_exists($b, 'getItemKey')) {$k = $b->getItemKey();} else {$k = $this->getItemKey($b);}$value = $b;} else if($value === $b) {if(method_exists($a, 'getItemKey')) {$k = $a->getItemKey();} else {$k = $this->getItemKey($a);}$value = $a;}if($k !== null) $key = $k;$data[$key] = $value;}$this->data = $data;} else if($a) {// b not already in array, so replace a with b$this->_insert($itemB, $a);$this->remove($a);} else if($b) {// a not already in array, so replace b with a$this->_insert($itemA, $b);$this->remove($b);}return $this;}/*** Set an item by key in the WireArray.** #pw-group-manipulation** @param int|string $key Key of item to set.* @param int|string|array|object|Wire $value Item value to set.* @throws WireException If given an item not compatible with this WireArray.* @return $this**/public function set($key, $value) {if(!$this->isValidItem($value)) {throw new WireException("Item '$key' set to " . get_class($this) . " is not an allowed type");}if(!$this->isValidKey($key)) {throw new WireException("Key '$key' is not an allowed key for " . get_class($this));}$this->trackChange($key, isset($this->data[$key]) ? $this->data[$key] : null, $value);$this->data[$key] = $value;$this->trackAdd($value, $key);return $this;}/*** Enables setting of WireArray elements in object notation.** Example: $myArray->myElement = 10;* Not applicable to numerically indexed arrays.** @param int|string $property Key of item to set.* @param int|string|array|object Value of item to set.* @throws WireException**/public function __set($property, $value) {if($this->getProperty($property)) {throw new WireException("Property '$property' is a reserved word and may not be set by direct reference.");}$this->set($property, $value);}/*** Ensures that isset() and empty() work for this classes properties.** @param string|int $key* @return bool**/public function __isset($key) {return isset($this->data[$key]);}/*** Ensures that unset() works for this classes data.** @param int|string $key**/public function __unset($key) {$this->remove($key);}/*** Like set() but accepts an array or WireArray to set multiple values at once** #pw-group-manipulation** @param array|WireArray $data Array or WireArray of data that you want to set.* @return $this**/public function setArray($data) {if(self::iterable($data)) {foreach($data as $key => $value) $this->set($key, $value);}return $this;}/*** Returns the value of the item at the given index, or null if not set.** You may also specify a selector, in which case this method will return the same result as* the `WireArray::findOne()` method. See the $key argument description for more details on* what can be provided.** #pw-group-retrieval** @param int|string|array $key Provide any of the following:* - Key of item to retrieve.* - Array of keys, in which case an array of matching items will be returned, indexed by your keys.* - A selector string or selector array, to return the first item that matches the selector.* - A string of text with "{var}" tags in it that will be populated with any matching properties from this WireArray.* - A string like "foobar[]" which returns an array of all "foobar" properties from each item in the WireArray.* - A string containing the "name" property of any item, and the matching item will be returned.* @return WireData|Page|mixed|array|null Value of item requested, or null if it doesn't exist.* @throws WireException**/public function get($key) {$match = null;// if an object was provided, get its keyif(is_object($key)) {/** @var object $key */$key = $this->getItemKey($key);/** @var string|int $key */}// if given an array of keys, return all matching itemsif(is_array($key)) {/** @var array $key */if(ctype_digit(implode('', array_keys($key)))) {$items = array();foreach($key as $k) {$item = $this->get($k);$items[$k] = $item;}return $items;} else {// selector array$item = $this->findOne($key);if($item === false) $item = null;return $item;}}// check if the index is set and return it if soif(isset($this->data[$key])) return $this->data[$key];// check if key contains something other than numbers, letters, underscores, hyphensif(is_string($key)) {if(!ctype_alnum($key) && !ctype_alnum(strtr($key, '-_', 'ab'))) {// check if key contains a selectorif(Selectors::stringHasSelector($key)) {$item = $this->findOne($key);if($item === false) $item = null;return $item;}if(strpos($key, '{') !== false && strpos($key, '}')) {// populate a formatted string with {tag} varsreturn wirePopulateStringTags($key, $this);}// check if key is requesting a property array: i.e. "name[]"if(strpos($key, '[]') !== false && substr($key, -2) == '[]') {return $this->explode(substr($key, 0, -2));}// check if key is asking for first match in "a|b|c"if(strpos($key, '|') !== false) {$numericKeys = $this->usesNumericKeys();foreach(explode('|', $key) as $k) {if(isset($this->data[$k])) {$match = $this->data[$k];} else if($numericKeys) {$match = $this->getItemThatMatches('name', $k);}if($match) break;}return $match;}}// if the WireArray uses numeric keys, then it's okay to// match a 'name' field if the provided key is a stringif(is_string($key) && $this->usesNumericKeys()) {$match = $this->getItemThatMatches('name', $key);}}return $match;}/*** Enables derefencing of WireArray elements in object notation.** Example: $myArray->myElement* Not applicable to numerically indexed arrays.* Fuel properties and hooked properties have precedence with this type of call.** @param int|string $property* @return Wire|WireData|Page|mixed|bool Value of item requested, or false if it doesn't exist.**/public function __get($property) {$value = parent::__get($property);if(is_null($value)) $value = $this->getProperty($property);if(is_null($value)) $value = $this->get($property);return $value;}/*** Get a predefined property of the array, or extra data that has been set.** Default properties include;** - `count` (int): Number of items present in this WireArray.* - `last` (mixed): Last item in this WireArray.* - `first` (mixed): First item in this WireArray.* - `keys` (array): Keys used in this WireArray.* - `values` (array): Values present in this WireArray.** These can also be accessed by direct reference.** ~~~~~* // Get count* $count = $items->getProperty('count');** // Same as above using direct access property* $count = $items->count;* ~~~~~** #pw-group-retrieval** @param string $property Name of property to retrieve* @return Wire|mixed**/public function getProperty($property) {static $properties = array(// property => method to map to'count' => 'count','last' => 'last','first' => 'first','keys' => 'getKeys','values' => 'getValues',);if(!in_array($property, $properties)) return $this->data($property);$func = $properties[$property];return $this->$func();}/*** Return the first item in this WireArray having a property named $key with $value, or NULL if not found.** Used internally by get() and has() methods.** @param string $key Property to match.* @param string|int|object $value $value to match.* @return Wire|null**/protected function getItemThatMatches($key, $value) {if(ctype_digit("$key")) return null;$item = null;foreach($this->data as $wire) {if(is_object($wire) && $wire instanceof Wire) {if($wire->$key === $value) {$item = $wire;break;}} else {if($wire === $value) {$item = $wire;break;}}}return $item;}/*** Does this WireArray have the given item, index, or match the given selector?** If the WireArray uses numeric keys, then this will also match a WireData object's "name" field.** ~~~~~* // See if it has a given $item* if($items->has($item)) {* // Has the given $item* }** // See if it has an object with a "name" property matching our text* if($items->has("name=something")) {* // Has an item with a "name" property equal to "something"* }** // Same as above, but works since "name" is assumed for many types* if($items->has("something")) {* // It has it* }* ~~~~~** #pw-group-retrieval* #pw-group-info** @param int|string|Wire $key Key of item to check or selector.* @return bool True if the item exists, false if not.**/public function has($key) {if(is_object($key)) {/** @var object|Wire $key */$key = $this->getItemKey($key);/** @var int|string $key */}if(is_array($key)) {// match selector arrayreturn $this->findOne($key) ? true : false;}if(array_key_exists($key, $this->data)) return true;$match = null;if(is_string($key)) {if(Selectors::stringHasOperator($key)) {$match = $this->findOne($key);} else if($this->usesNumericKeys()) {$match = $this->getItemThatMatches('name', $key);}}return $match ? true : false;}/*** Get a PHP array of all the items in this WireArray with original keys maintained** #pw-group-retrieval** @return array Copy of the array that WireArray uses internally.* @see WireArray::getValues()**/public function getArray() {return $this->data;}/*** Returns all items in the WireArray (for syntax convenience)** This is for syntax convenience, as it simply returns this instance of the WireArray.** #pw-group-retrieval** @return $this**/public function getAll() {return $this;}/*** Returns a regular PHP array of all keys used in this WireArray.** #pw-group-retrieval** @return array Keys used in the WireArray.**/public function getKeys() {return array_keys($this->data);}/*** Returns a regular PHP array of all values used in this WireArray.** Unlike the `WireArray::getArray()` method, this does not attempt to maintain original* keys of the items. The returned array is reindexed from 0.** #pw-group-retrieval** @return array|Wire[] Values used in the WireArray.* @see WireArray::getArray()**/public function getValues() {return array_values($this->data);}/*** Get a random item from this WireArray.** - If one item is requested (default), the item is returned (unless `$alwaysArray` argument is true).* - If multiple items are requested, a new `WireArray` of those items is returned.* - We recommend using this method when you just need 1 random item, and using the `WireArray::findRandom()` method* when you need multiple random items.** ~~~~~* // Get a single random item* $randomItem = $items->getRandom();** // Get 3 random items* $randomItems = $items->getRandom(3);* ~~~~~** #pw-group-retrieval** @param int $num Number of items to return. Optional and defaults to 1.* @param bool $alwaysArray If true, then method will always return an array of items, even if it only contains 1 item.* @return WireArray|Wire|mixed|null Returns value of item, or new WireArray of items if more than one requested.* @see WireArray::findRandom(), WireArray::findRandomTimed()**/public function getRandom($num = 1, $alwaysArray = false) {$items = $this->makeNew();if($num < 1) return $items;$count = $this->count();if(!$count) {if($num == 1 && !$alwaysArray) return null;return $items;}$keys = array_rand($this->data, ($num > $count ? $count : $num));if($num == 1 && !$alwaysArray) return $this->data[$keys];if(!is_array($keys)) $keys = array($keys);foreach($keys as $key) $items->add($this->data[$key]);$items->setTrackChanges(true);return $items;}/*** Find a specified quantity of random elements from this WireArray.** Unlike `WireArray::getRandom()` this method always returns a WireArray (or derived type).** ~~~~~* // Get 3 random items* $randomItems = $items->findRandom(3);* ~~~~~** #pw-group-retrieval** @param int $num Number of items to return* @return WireArray* @see WireArray::getRandom(), WireArray::findRandomTimed()**/public function findRandom($num) {return $this->getRandom((int) $num, true);}/*** Find a quantity of random elements from this WireArray based on a timed interval (or user provided seed).** If no `$seed` is provided, today's date (day) is used to seed the random number* generator, so you can use this function to rotate items on a daily basis.** _Idea and implementation provided by [mindplay.dk](https://twitter.com/mindplaydk)_** ~~~~~* // Get same 3 random items per day* $randomItems = $items->findRandomTimed(3);** // Get same 3 random items per hour* $randomItems = $items->findRandomTimed('YmdH');* ~~~~~** #pw-group-retrieval** @param int $num The amount of items to extract from the given list* @param int|string $seed Optionally provide one of the following:* - A PHP [date()](http://php.net/manual/en/function.date.php) format string.* - A number used to see the random number generator.* - The default is the PHP date format "Ymd" which makes it randomize once daily.* @return WireArray* @see WireArray::findRandom()**/public function findRandomTimed($num, $seed = 'Ymd') {if(is_string($seed)) $seed = crc32(date($seed));srand($seed);$keys = $this->getKeys();$items = $this->makeNew();while(count($keys) > 0 && count($items) < $num) {$index = rand(0, count($keys)-1);$key = $keys[$index];$items->add($this->get($key));array_splice($keys, $index, 1);}return $items;}/*** Get a slice of the WireArray.** Given a starting point and a number of items, returns a new WireArray of those items.* If `$limit` is omitted, then it includes everything beyond the starting point.** ~~~~~* // Get first 3 items* $myItems = $items->slice(0, 3);* ~~~~~** #pw-group-retrieval** @param int $start Starting index.* @param int $limit Number of items to include. If omitted, includes the rest of the array.* @return WireArray Returns a new WireArray.**/public function slice($start, $limit = 0) {if($limit) {$slice = array_slice($this->data, $start, $limit);} else {$slice = array_slice($this->data, $start);}$items = $this->makeNew();$items->import($slice);$items->setTrackChanges(true);return $items;}/*** Prepend an item to the beginning of the WireArray.** ~~~~~* // Add item to beginning* $items->prepend($item);* ~~~~~** #pw-group-manipulation** @param Wire|WireArray|mixed $item Item to prepend.* @return $this This instance.* @throws WireException* @see WireArray::append()**/public function prepend($item) {if(!$this->isValidItem($item)) {if($item instanceof WireArray) {foreach(array_reverse($item->getArray()) as $i) $this->prepend($i);return $this;} else {throw new WireException("Item prepend to " . get_class($this) . " is not an allowed type");}}if($this->duplicateChecking && ($key = $this->getItemKey($item)) !== null) {// item already present$a = array($key => $item);$this->data = $a + $this->data; // UNION operator for arrays// $this->data = array_merge($a, $this->data);} else {// new itemarray_unshift($this->data, $item);reset($this->data);$key = key($this->data);}$this->trackChange('prepend', null, $item);$this->trackAdd($item, $key);return $this;}/*** Append an item to the end of the WireArray** This is a functionally identical alias of the `WireArray::add()` method here for* naming consistency with the `WireArray::prepend()` method.** ~~~~~* // Add item to end* $items->append($item);* ~~~~~** #pw-group-manipulation** @param Wire|WireArray|mixed $item Item to append.* @return $this This instance.* @see WireArray::prepend(), WireArray::add()**/public function append($item) {$this->add($item);return $this;}/*** Unshift an element to the beginning of the WireArray (alias for prepend)** This is for consistency with PHP's naming convention of the `array_unshift()` method.** #pw-group-manipulation** @param Wire|WireArray|mixed $item Item to prepend.* @return $this This instance.* @see WireArray::shift(), WireArray::prepend()**/public function unshift($item) {return $this->prepend($item);}/*** Shift an element off the beginning of the WireArray and return it** Consistent with behavior of PHP's `array_shift()` method.** #pw-group-manipulation* #pw-group-retrieval** @return Wire|mixed|null Item shifted off the beginning or NULL if empty.* @see WireArray::unshift()**/public function shift() {reset($this->data);$key = key($this->data);$item = array_shift($this->data);if(is_null($item)) return $item;$this->trackChange('shift', $item, null);$this->trackRemove($item, $key);return $item;}/*** Push an item to the end of the WireArray.** Same as `WireArray::add()` and `WireArray::append()`, but here for syntax convenience.** #pw-group-manipulation** @param Wire|mixed $item Item to push.* @return $this This instance.* @see WireArray::pop()**/public function push($item) {$this->add($item);return $this;}/*** Pop an element off the end of the WireArray and return it** #pw-group-retrieval* #pw-group-manipulation** @return Wire|mixed|null Item popped off the end or NULL if empty.**/public function pop() {end($this->data);$key = key($this->data);$item = array_pop($this->data);if(is_null($item)) return $item;$this->trackChange('pop', $item, null);$this->trackRemove($item, $key);return $item;}/*** Shuffle/randomize this WireArray** #pw-group-manipulation** @return $this This instance.**/public function shuffle() {$keys = $this->getKeys();$data = array();// shuffle the keys rather than the original array in case it's associative// because PHP's shuffle reindexes the arrayshuffle($keys);foreach($keys as $key) {$data[$key] = $this->data[$key];}$this->trackChange('shuffle', $this->data, $data);$this->data = $data;return $this;}/*** Returns a new WireArray of the item at the given index.** Unlike `WireArray::get()` this returns a new WireArray with a single item, or a blank WireArray if item doesn't exist.* Applicable to numerically indexed WireArray only.** #pw-group-retrieval** @param int $num Index number* @return WireArray* @see WireArray::eq()**/public function index($num) {return $this->slice($num, 1);}/*** Returns the item at the given index starting from 0, or NULL if it doesn't exist.** Unlike the `WireArray::index()` method, this returns an actual item and not another WireArray.** #pw-group-retrieval** @param int $num Return the n'th item in this WireArray. Specify a negative number to count from the end rather than the start.* @return Wire|null* @see WireArray::index()**/public function eq($num) {$num = (int) $num;$item = array_slice($this->data, $num, 1);$item = count($item) ? reset($item) : null;return $item;}/*** Returns the first item in the WireArray or boolean false if empty.** Note that this resets the internal WireArray pointer, which would affect other active iterations.** ~~~~~* $item = $items->first();* ~~~~~** #pw-group-traversal* #pw-group-retrieval** @return Wire|mixed|bool**/public function first() {return reset($this->data);}/*** Returns the last item in the WireArray or boolean false if empty.** Note that this resets the internal WireArray pointer, which would affect other active iterations.** ~~~~~* $item = $items->last();* ~~~~~** #pw-group-traversal* #pw-group-retrieval** @return Wire|mixed|bool**/public function last() {return end($this->data);}/*** Removes the given item or index from the WireArray (if it exists).** #pw-group-manipulation** @param int|string|Wire $key Item to remove (object), or index of that item, or (3.0.196+) selector string.* @return $this This instance.**/public function remove($key) {$obj = is_object($key);if($obj) {$key = $this->getItemKey($key);}if(array_key_exists($key, $this->data)) {$item = $this->data[$key];unset($this->data[$key]);$this->trackChange("remove", $item, null);$this->trackRemove($item, $key);} else if(!$obj && is_string($key) && Selectors::stringHasSelector($key)) {foreach($this->find($key) as $item) {$this->remove($item);}}return $this;}/*** Removes multiple identified items at once** #pw-group-manipulation** @param array|Wire|string|WireArray $items Items to remove* @return $this**/public function removeItems($items) {if(!self::iterable($items)) $items = array($items);foreach($items as $item) $this->remove($item);return $this;}/*** Removes all items from the WireArray, leaving it blank** #pw-group-manipulation** @return $this**/public function removeAll() {foreach($this as $key => $value) {$this->remove($key);}return $this;}/*** Remove an item without any record of the event or telling anything else.** #pw-internal** @param int|string|Wire $key Index of item or object instance of item.* @return $this This instance.**/public function removeQuietly($key) {if(is_object($key)) $key = $this->getItemKey($key);unset($this->data[$key]);return $this;}/*** Sort this WireArray by the given properties.** - Sort properties can be given as a string in the format `name, datestamp` or as an array of strings,* i.e. `["name", "datestamp"]`.** - You may also specify the properties as `property.subproperty`, where property resolves to a Wire derived object* in each item, and subproperty resolves to a property within that object.** - Prepend or append a minus "-" to reverse the sort (per field).** ~~~~~* // Sort newest to oldest* $items->sort("-created");** // Sort by last_name then first_name* $items->sort("last_name, first_name");* ~~~~~** #pw-group-manipulation** @param string|array $properties Field names to sort by (CSV string or array).* @param int|null $flags Optionally specify sort flags (see sortFlags method for details).* @return $this reference to current instance.*/public function sort($properties, $flags = null) {$_flags = $this->sortFlags; // rememberif(is_int($flags)) $this->sortFlags($flags);$result = $this->_sort($properties);if(is_int($flags) && $flags !== $_flags) $this->sortFlags($_flags); // restorereturn $result;}/*** Sort this WireArray by the given properties (internal use)** This function contains additions and modifications by @niklaka.** $properties can be given as a sortByField string, i.e. "name, datestamp" OR as an array of strings, i.e. array("name", "datestamp")* You may also specify the properties as "property.subproperty", where property resolves to a Wire derived object,* and subproperty resolves to a property within that object.** @param string|array $properties Field names to sort by (comma separated string or an array). Prepend or append a minus "-" to reverse the sort (per field).* @param int $numNeeded *Internal* amount of rows that need to be sorted (optimization used by filterData)* @return $this reference to current instance.*/protected function _sort($properties, $numNeeded = null) {// string version is used for change tracking$isArray = is_array($properties);$propertiesStr = $isArray ? implode(',', $properties) : $properties;if(!$isArray) $properties = explode(',', $properties);if(empty($properties)) return $this;// shortcut for random (only allowed as the sole sort property)// no warning/error for issuing more properties though// TODO: warning for random+more properties (and trackChange() too)if($properties[0] === 'random') return $this->shuffle();$data = $this->stableSort($this, $properties, $numNeeded);if($this->trackChanges) $this->trackChange("sort:$propertiesStr", $this->data, $data);$this->data = $data;return $this;}/*** Get or set sort flags that affect behavior of any sorting functions** The following constants may be used when setting the sort flags:** - `SORT_REGULAR` compare items normally (don’t change types)* - `SORT_NUMERIC` compare items numerically* - `SORT_STRING` compare items as strings* - `SORT_LOCALE_STRING` compare items as strings, based on the current locale* - `SORT_NATURAL` compare items as strings using “natural ordering” like natsort()* - `SORT_FLAG_CASE` can be combined (bitwise OR) with SORT_STRING or SORT_NATURAL to sort strings case-insensitively* - `SORT_APPEND_NULLS` can be used on its own or combined with any of above (bitwise OR) to specify that null* or blank values should be treated as unsortable and appended to the end of the sortable set rather than sorted as* blank values. This duplicates the behavior prior to 3.0.194 (available only in 3.0.194+). Note that this flag* is unique to ProcessWire only and is not in PHP.** For more details, see `$sort_flags` argument at: https://www.php.net/manual/en/function.sort.php** #pw-group-manipulation** @param bool $sortFlags Optionally specify flag(s) to set* @return int Returns current flags* @since 3.0.129**/public function sortFlags($sortFlags = false) {if(is_int($sortFlags)) $this->sortFlags = $sortFlags;return $this->sortFlags;}/*** Sort given array by first given property.** This function contains additions and modifications by @niklaka.** @param array|WireArray &$data Reference to an array to sort.* @param array $properties Array of properties: first property is used now and others in recursion, if needed.* @param int $numNeeded *Internal* amount of rows that need to be sorted (optimization used by filterData)* @return array Sorted array (at least $numNeeded items, if $numNeeded is given)*/protected function stableSort(&$data, $properties, $numNeeded = null) {$property = trim(array_shift($properties));$nullable = array();$sortable = array();$reverse = false;$subProperty = '';$sortFlags = $this->sortFlags;$sortNulls = true;if($sortFlags >= SORT_APPEND_NULLS && ($sortFlags & SORT_APPEND_NULLS)) {$sortNulls = false;$sortFlags -= SORT_APPEND_NULLS;}$pos = strpos($property, '-');if($pos !== false && ($pos === 0 || substr($property, -1) == '-')) {$reverse = true;$property = trim($property, '-');}$pos = strpos($property, '.');if($pos) {list($property, $subProperty) = explode('.', $property, 2);}foreach($data as $item) {/** @var Wire $item */$key = $this->getItemPropertyValue($item, $property);// if item->property resolves to another Wire, then try to get the subProperty from that Wire (if it exists)if($key instanceof Wire && $subProperty) {$key = $this->getItemPropertyValue($key, $subProperty);}if($key === null) {if($sortNulls) {$key = "\0"; // sort as ascii null} else {$nullable[] = $item;continue;}} else if($key === false) {$key = 0;} else if($key === true) {$key = 1;} else if(is_int($key)) {// ok} else if(ctype_digit("$key")) {$key = (int) "$key";} else {$key = (string) $key;if(trim($key) === '') {if($sortNulls) {$key = ' '; // ensure sort value higher than \0} else {$nullable[] = $item;continue;}}}if(isset($sortable[$key])) {// key resolved to the same value that another did, so keep them together by converting this index to an array// this makes the algorithm stable (for equal keys the order would be undefined)if(is_array($sortable[$key])) {$sortable[$key][] = $item;} else {$sortable[$key] = array($sortable[$key], $item);}} else {$sortable[$key] = $item;}}// sort the items by the keys we collectedif($reverse) {krsort($sortable, $sortFlags);} else {ksort($sortable, $sortFlags);}// add the items that resolved to no key to the end, as an arrayif(!empty($nullable)) $sortable[] = $nullable;// restore sorted array to lose sortable keys and restore proper keys$a = array();foreach($sortable as $key => $value) {if(is_array($value)) {// if more properties to sort by exist, use them for this sub-array$n = null;if($numNeeded) $n = $numNeeded - count($a);if(count($properties)) {$value = $this->stableSort($value, $properties, $n);}foreach($value as $k => $v) {$newKey = $this->getItemKey($v);$a[$newKey] = $v;// are we done yet?if($numNeeded && count($a) > $numNeeded) break;}} else {$newKey = $this->getItemKey($value);$a[$newKey] = $value;}// are we done yet?if($numNeeded && count($a) > $numNeeded) break;}return $a;}/*** Get the value of $property from $item** Used by the WireArray::sort method to retrieve a value from a Wire object.* Primarily here as a template method so that it can be overridden.* Lets it prepare the Wire for any states needed for sorting.** @param Wire $item* @param string $property* @return mixed**/protected function getItemPropertyValue(Wire $item, $property) {if(strpos($property, '.') !== false) return WireData::_getDot($property, $item);return $item->$property;}/*** Filter out Wires that don't match the selector.** This is applicable to and destructive to the WireArray.* This function contains additions and modifications by @niklaka.** @param string|array|Selectors $selectors Selector string|array to use as the filter.* @param bool|int $not Make this a "not" filter? Use int 1 for “not all” mode as if selectors had brackets around it. (default is false)* @return $this reference to current [filtered] instance**/protected function filterData($selectors, $not = false) {if(is_object($selectors) && $selectors instanceof Selectors) {$selectors = clone $selectors;} else {if(!is_array($selectors) && ctype_digit("$selectors")) $selectors = "id=$selectors";$selector = $selectors;$selectors = $this->wire(new Selectors());$selectors->init($selector);}$this->filterDataSelectors($selectors);$fields = $this->wire()->fields;$sort = array();$start = 0;$limit = null;$eq = null;$notAll = $not === 1;if($notAll) $not = true;// leave sort, limit and start away from filtering selectorsforeach($selectors as $selector) {$remove = true;$field = $selector->field;if($field === 'sort') {// use all sort selectors$sort[] = $selector->value;} else if($field === 'start') {// use only the last start selector$start = (int) $selector->value;} else if($field === 'limit') {// use only the last limit selector$limit = (int) $selector->value;} else if(($field === 'index' || $field == 'eq') && !$fields->get($field)) {// eq or index propertiesswitch($selector->value) {case 'first': $eq = 0; break;case 'last': $eq = -1; break;default: $eq = (int) $selector->value;}} else {// everything else is to be saved for filtering$remove = false;}if($remove) $selectors->remove($selector);}// now filter the data according to the selectors that remainforeach($this->data as $key => $item) {$qty = 0;$qtyMatch = 0;foreach($selectors as $selector) {$qty++;if(is_array($selector->field)) {$value = array();foreach($selector->field as $field) {$value[] = (string) $this->getItemPropertyValue($item, $field);}} else {$value = (string) $this->getItemPropertyValue($item, $selector->field);}if($not === $selector->matches($value) && isset($this->data[$key])) {$qtyMatch++;if($notAll) continue; // will do this outside the loop of all in $selectors match$this->trackRemove($this->data[$key], $key);unset($this->data[$key]);}}if($notAll && $qty && $qty === $qtyMatch) {$this->trackRemove($this->data[$key], $key);unset($this->data[$key]);}}if(!is_null($eq)) {if($eq === -1) {$limit = -1;$start = null;} else if($eq === 0) {$start = 0;$limit = 1;} else {$start = $eq;$limit = 1;}}if($limit < 0 && $start < 0) {// we don't support double negative, so double negative makes a positive$start = abs($start);$limit = abs($limit);} else {if($limit < 0) {if($start) {$start = $start - abs($limit);$limit = abs($limit);} else {$start = count($this->data) - abs($limit);$limit = count($this->data);}}if($start < 0) {$start = count($this->data) - abs($start);}}// if $limit has been given, tell sort the amount of rows that will be usedif(count($sort)) $this->_sort($sort, $limit ? $start+$limit : null);if($start || $limit) {$this->data = array_slice($this->data, $start, $limit, true);}if($this->trackChanges()) $this->trackChange("filterData:$selectors");return $this;}/*** Prepare selectors for filtering** Template method for descending classes to modify selectors if needed** @param Selectors $selectors**/protected function filterDataSelectors(Selectors $selectors) { }/*** Filter this WireArray to only include items that match the given selector (destructive)** ~~~~~* // Filter $items to contain only those with "featured" property having value 1* $items->filter("featured=1");* ~~~~~** #pw-group-manipulation** @param string|array|Selectors $selector Selector string or array to use as the filter.* @return $this reference to current instance.* @see filterData**/public function filter($selector) {// Same as filterData, but for public interface without the $not option.return $this->filterData($selector, false);}/*** Filter this WireArray to only include items that DO NOT match the selector (destructive)** ~~~~~* // returns all pages that don't have a 'nonav' variable set to a positive value.* $pages->not("nonav");* ~~~~~** #pw-group-manipulation** @param string|array|Selectors $selector* @return $this reference to current instance.* @see filterData**/public function not($selector) {// Same as filterData, but for public interface with the $not option specifically set to "true".return $this->filterData($selector, true);}/*** Like the not() method but $selector evaluated as if it had (brackets) around it** #pw-internal Until we've got a better description for what this does** @param string|array|Selectors $selector* @return $this reference to current instance.* @see filterData**/public function notAll($selector) {return $this->filterData($selector, 1);}/*** Find all items in this WireArray that match the given selector.** This is non destructive and returns a brand new WireArray.** ~~~~~* // Find all items with a title property containing the word "foo"* $matches = $items->find("title%=foo");* if($matches->count()) {* echo "Found $matches->count items";* } else {* echo "Sorry, no items were found";* }* ~~~~~** #pw-group-retrieval** @param string|array|Selectors $selector* @return WireArray**/public function find($selector) {$a = $this->makeCopy();if(empty($selector)) return $a;$a->filter($selector);return $a;}/*** Find a single item by selector** This is the same as `WireArray::find()` except that it returns a single* item rather than a new WireArray of items.** ~~~~~* // Get an item with name "foo-bar"* $item = $items->findOne("name=foo-bar");* if($item) {* // item was found* } else {* // item was not found* }* ~~~~~** #pw-group-retrieval** @param string|array|Selectors $selector* @return Wire|bool Returns item from WireArray or false if the result is empty.* @see WireArray::find()**/public function findOne($selector) {return $this->find($selector)->first();}/*** Determines if the given item iterable as an array.** - Returns true for arrays and WireArray derived objects.* - Can be called statically like this `WireArray::iterable($a)`.** #pw-group-info** @param mixed $item Item to check for iterability.* @return bool True if item is an iterable array or WireArray (or subclass of WireArray).**/public static function iterable($item) {if(is_array($item)) return true;if($item instanceof WireArray) return true;return false;}/*** Allows iteration of the WireArray.** - Fulfills PHP's IteratorAggregate interface so that you can traverse the WireArray.* - No need to call this method directly, just use PHP's `foreach()` method on the WireArray.** ~~~~~* // Traversing a WireArray with foreach:* foreach($items as $item) {* // ...* }* ~~~~~** #pw-group-traversal** @return \ArrayObject|Wire[]**/#[\ReturnTypeWillChange]public function getIterator() {return new \ArrayObject($this->data);}/*** Returns the number of items in this WireArray.** Fulfills PHP's Countable interface, meaning it also enables this WireArray to be used with PHP's `count()` function.** ~~~~~* // These two are the same* $qty = $items->count();* $qty = count($items);* ~~~~~** #pw-group-retrieval** @return int**/#[\ReturnTypeWillChange]public function count() {return count($this->data);}/*** Sets an index in the WireArray.** For the \ArrayAccess interface.** #pw-internal** @param int|string $key Key of item to set.* @param Wire|mixed $value Value of item.**/#[\ReturnTypeWillChange]public function offsetSet($key, $value) {$this->set($key, $value);}/*** Returns the value of the item at the given index, or false if not set.** #pw-internal** @param int|string $key Key of item to retrieve.* @return Wire|mixed|bool Value of item requested, or false if it doesn't exist.**/#[\ReturnTypeWillChange]public function offsetGet($key) {if($this->offsetExists($key)) {return $this->data[$key];} else {return false;}}/*** Unsets the value at the given index.** For the \ArrayAccess interface.** #pw-internal** @param int|string $key Key of the item to unset.* @return bool True if item existed and was unset. False if item didn't exist.**/#[\ReturnTypeWillChange]public function offsetUnset($key) {if($this->offsetExists($key)) {$this->remove($key);return true;} else {return false;}}/*** Determines if the given index exists in this WireArray.** For the \ArrayAccess interface.** #pw-internal** @param int|string $key Key of the item to check for existance.* @return bool True if the item exists, false if not.**/#[\ReturnTypeWillChange]public function offsetExists($key) {return array_key_exists($key, $this->data);}/*** Returns a string representation of this WireArray.** @return string**/public function __toString() {$s = '';foreach($this as $key => $value) {if(is_array($value)) $value = "array(" . count($value) . ")";$s .= "$value|";}$s = rtrim($s, '|');return $s;}/*** Return a new reversed version of this WireArray.** #pw-group-retrieval** @return WireArray**/public function reverse() {$a = $this->makeNew();$a->import(array_reverse($this->data, true));return $a;}/*** Return a new array that is unique (no two of the same elements)** This is the equivalent to PHP's [array_unique()](http://php.net/manual/en/function.array-unique.php) function.** #pw-group-retrieval** @param int $sortFlags Sort flags per PHP's `array_unique()` function (default=`SORT_STRING`)* @return WireArray**/public function unique($sortFlags = SORT_STRING) {$a = $this->makeNew();$a->import(array_unique($this->data, $sortFlags));return $a;}/*** Clears out any tracked changes and turns change tracking ON or OFF** #pw-internal** @param bool $trackChanges True to turn change tracking ON, or false to turn OFF. Default of true is assumed.* @return Wire|WireArray**/public function resetTrackChanges($trackChanges = true) {$this->itemsAdded = array();$this->itemsRemoved = array();return parent::resetTrackChanges($trackChanges);}/*** Track an item added** @param Wire|mixed $item* @param int|string $key**/protected function trackAdd($item, $key) {if($key) {}if($this->trackChanges()) $this->itemsAdded[] = $item;// wire this WireArray to the same instance of $item, if it isn’t already wiredif($this->_wire === null && $item instanceof Wire && $item->isWired()) $item->wire($this);}/*** Track an item removed** @param Wire|mixed $item* @param int|string $key**/protected function trackRemove($item, $key) {if($key) {}if($this->trackChanges()) $this->itemsRemoved[] = $item;}/*** Return array of all items added to this WireArray (while change tracking is enabled)** #pw-group-changes** @return array|Wire[]**/public function getItemsAdded() {return $this->itemsAdded;}/*** Return array of all items removed from this WireArray (when change tracking is enabled)** #pw-group-changes** @return array|Wire[]**/public function getItemsRemoved() {return $this->itemsRemoved;}/*** Given an item, get the item that comes after it in the WireArray** #pw-group-retrieval** @param Wire $item* @param bool $strict If false, string comparison will be used rather than exact instance comparison.* @return Wire|null Returns next item if found, or null if not**/public function getNext($item, $strict = true) {if(!$this->isValidItem($item)) return null;$key = $this->getItemKey($item);$useStr = false;if($key === null) {if($strict) return null;$key = (string) $item;$useStr = true;}$getNext = false;$nextItem = null;foreach($this->data as $k => $v) {if($getNext) {$nextItem = $v;break;}if($useStr) $k = (string) $v;if($k === $key) $getNext = true;}return $nextItem;}/*** Given an item, get the item before it in the WireArray** #pw-group-retrieval** @param Wire $item* @param bool $strict If false, string comparison will be used rather than exact instance comparison.* @return Wire|null Returns item that comes before given item, or null if not found**/public function getPrev($item, $strict = true) {if(!$this->isValidItem($item)) return null;$key = $this->getItemKey($item);$useStr = false;if($key === null) {if($strict) return null;$key = (string) $item;$useStr = true;}$prevItem = null;$lastItem = null;foreach($this->data as $k => $v) {if($useStr) $k = (string) $v;if($k === $key) {$prevItem = $lastItem;break;}$lastItem = $v;}return $prevItem;}/*** Does this WireArray use numeric keys only?** We determine this by creating a blank item and seeing what the type is of it's key.** #pw-internal** @return bool**/protected function usesNumericKeys() {static $testItem = null;static $usesNumericKeys = null;if(!is_null($usesNumericKeys)) return $usesNumericKeys;if(is_null($testItem)) $testItem = $this->makeBlankItem();if(is_null($testItem)) return true;$key = $this->getItemKey($testItem);$usesNumericKeys = is_int($key) ? true : false;return $usesNumericKeys;}/*** Combine all elements into a delimiter-separated string containing the given property from each item** Similar to PHP's `implode()` function.** #pw-link [Introduction of implode method](https://processwire.com/talk/topic/5098-new-wirearray-api-additions-on-dev/)* #pw-group-retrieval* #pw-group-fun-tools* #pw-group-output-rendering** @param string $delimiter The delimiter to separate each item by (or the glue to tie them together).* If not needed, this argument may be omitted and $property supplied first (also shifting $options to 2nd arg).* @param string|callable $property The property to retrieve from each item, or a function that returns the value to store.* If a function/closure is provided it is given the $item (argument 1) and the $key (argument 2), and it should* return the value (string) to use. If delimiter is omitted, this becomes the first argument.* @param array $options Optional options to modify the behavior:* - `skipEmpty` (bool): Whether empty items should be skipped (default=true)* - `prepend` (string): String to prepend to result. Ignored if result is blank.* - `append` (string): String to append to result. Ignored if result is blank.* - Please note that if delimiter is omitted, $options becomes the second argument.* @return string* @see WireArray::each(), WireArray::explode()**/public function implode($delimiter, $property = '', array $options = array()) {$defaults = array('skipEmpty' => true,'prepend' => '','append' => '');if(!count($this->data)) return '';$firstItem = reset($this->data);$itemIsObject = is_object($firstItem);if(!is_string($delimiter) && is_callable($delimiter)) {// first delimiter argument omitted and a function was supplied// property is assumed to be blankif(is_array($property)) $options = $property;$property = $delimiter;$delimiter = '';} else if($itemIsObject && (empty($property) || is_array($property))) {// delimiter was omitted, forcing $property to be first argif(is_array($property)) $options = $property;$property = $delimiter;$delimiter = '';}$options = array_merge($defaults, $options);$isFunction = !is_string($property) && is_callable($property);$str = '';$n = 0;foreach($this as $key => $item) {if($isFunction) {$value = $property($item, $key);} else if(strlen($property) && $itemIsObject) {$value = $item->get($property);} else {$value = $item;}if(is_array($value)) $value = 'array(' . count($value) . ')';$value = (string) $value;if(!strlen($value) && $options['skipEmpty']) continue;if($n) $str .= $delimiter;$str .= $value;$n++;}if(strlen($str) && ($options['prepend'] || $options['append'])) {$str = "$options[prepend]$str$options[append]";}return $str;}/*** Return a plain array of the requested property from each item** You may provide an array of properties as the $property, in which case it will return an* array of associative arrays with all requested properties for each item.** You may also provide a function as the $property. That function receives the $item* as the first argument and $key as the second. It should return the value that will be stored.** The keys of the returned array remain consistent with the original WireArray.** #pw-link [Introduction of explode method](https://processwire.com/talk/topic/5098-new-wirearray-api-additions-on-dev/)* #pw-group-retrieval* #pw-group-fun-tools** @param string|callable|array $property Property or properties to retrieve, or callable function that should receive items.* @param array $options Options to modify default behavior:* - `getMethod` (string): Method to call on each item to retrieve $property (default = "get")* - `key` (string|null): Property of Wire objects to use for key of array, or omit (null) for non-associative array (default).* @return array* @see WireArray::each(), WireArray::implode()**/public function explode($property = '', array $options = array()) {$defaults = array('getMethod' => 'get', // method used to get value from each item'key' => null,);$options = array_merge($defaults, $options);$getMethod = $options['getMethod'];$isArray = is_array($property);$isFunction = !$isArray && !is_string($property) && is_callable($property);$values = array();foreach($this as $key => $item) {if(!is_object($item)) {$values[$key] = $item;continue;}if(!empty($options['key']) && is_string($options['key'])) {$key = $item->get($options['key']);if(!is_string($key) || !is_int($key)) $key = (string) $key;if(!strlen($key)) continue;if(isset($values[$key])) continue;}if($isFunction) {$values[$key] = $property($item, $key);} else if($isArray) {$values[$key] = array();foreach($property as $p) {$values[$key][$p] = $getMethod == 'get' ? $item->get($p) : $item->$getMethod($p);}} else {$values[$key] = $getMethod == 'get' ? $item->get($property) : $item->$getMethod($property);}}return $values;}/*** Return a new copy of this WireArray with the given item(s) appended** Primarily for syntax convenience in fluent interfaces.** ~~~~~* if($page->parents->and($page)->has($featured)) {* // either $page or its parents has the $featured page* }* ~~~~~** #pw-group-traversal* #pw-group-fun-tools* #pw-link [Introduction of and method](https://processwire.com/talk/topic/5098-new-wirearray-api-additions-on-dev/)** @param Wire|WireArray $item Item(s) to append* @return WireArray New WireArray containing this one and the given item(s).**/public function ___and($item) {$a = $this->makeCopy();$a->add($item);return $a;}/*** Store or retrieve an extra data value in this WireArray** The data() function is exactly the same thing that it is in jQuery: <http://api.jquery.com/data/>.** ~~~~~~* // set a data value named 'foo' to value 'bar'* $a->data('foo', 'bar');** // retrieve the previously set data value* $bar = $a->data('foo');** // get all previously set data* $all = $a->data();* ~~~~~~** #pw-group-other-data-storage* #pw-link [Introduction of data method](https://processwire.com/talk/topic/5098-new-wirearray-api-additions-on-dev/)** @param string|null|array|bool $key Name of data property you want to get or set, or:* - Omit to get all data properties.* - Specify associative array of [property => value] to set multiple properties.* - Specify associative array and boolean TRUE for $value argument to replace all data with the new array given in $key.* - Specify regular array of property names to return multiple properties.* - Specify boolean FALSE to unset property name specified in $value argument.* @param mixed|null|bool $value Value of data property you want to set. Omit when getting properties.* - Specify boolean TRUE to replace all data with associative array of data given in $key argument.* @return WireArray|mixed|array|null Returns one of the following, depending on specified arguments:* - `mixed` when getting a single property: whatever you set is what you will get back.* - `null` if the property you are trying to get does not exist in the data.* - `$this` reference to this WireArray if you were setting a value.* - `array` of all data if you specified no arguments or requested multiple keys.**/public function data($key = null, $value = null) {if($key === null && $value === null) {// get all propertiesreturn $this->extraData;} else if(is_array($key)) {// get or set multiple propertiesif($value === true) {// replace all data with data in given $key array$this->extraData = $key;} else {// test if array is associativeif(ctype_digit(implode('0', array_keys($key)))) {// regular, non-associative array, GET only requested properties$a = array();foreach($key as $k) {$a[$k] = isset($this->extraData[$k]) ? $this->extraData[$k] : null;}return $a;} else if(count($key)) {// associative array, setting multiple values to extraData$this->extraData = array_merge($this->extraData, $key);}}} else if($key === false && is_string($value)) {// unset a propertyunset($this->extraData[$value]);} else if($value === null) {// get a propertyreturn isset($this->extraData[$key]) ? $this->extraData[$key] : null;} else {// set a property$this->extraData[$key] = $value;}return $this;}/*** Remove a property/value previously set with the WireArray::data() method.** #pw-group-other-data-storage** @param string $key Name of property you want to remove* @return $this**/public function removeData($key) {unset($this->extraData[$key]);return $this;}/*** Enables use of $var('key')** @param string $key* @return mixed**/public function __invoke($key) {if(in_array($key, array('first', 'last', 'count'))) return $this->$key();if(is_int($key) || ctype_digit($key)) {if($this->usesNumericKeys()) {// if keys are already numeric, we use themreturn $this->get((int) $key);} else {// if keys are not numeric, we delegete numbers to eq(n)return $this->eq((int) $key);}} else if(is_callable($key) || (is_string($key) && strpos($key, '{') !== false && strpos($key, '}'))) {return $this->each($key);}return $this->get($key);}/*** Handler for when an unknown/unhooked method call is executed** If interested in hooking this, please see the `Wire::callUnknown()` method for more* details on the purpose and potential hooking implementation of this method.** The implementation built-in to WireArray provides a couple of handy capabilities to all* WireArray derived classes (assuming that `$items` is an instance of any WireArray):** - It enables you to call `$items->foobar()` and receive a regular PHP array* containing the value of the "foobar" property from each item in this WireArray.* It is equivalent to calling `$items->explode('foobar')`. Of course, substitute* "foobar" with the name of any property present on items in the WireArray.** - It enables you to call `$items->foobar(", ")` and receive a string containing* the value of the "foobar" property from each item, delimited by the string you* provided as an argument (a comma and space ", " in this case). This is equivalent* to calling `$items->implode(", ", "foobar")`.** - Also note that if you call `$items->foobar(", ", $options)` where $options is an* array, it is equivalent to `$items->implode(", ", "foobar", $options)`.** ~~~~~* // Get array of all "title" values from each item* $titlesArray = $items->title();** // Get a newline separated string of all "title" values from each item* $titlesString = $items->title("\n");* ~~~~~** #pw-hooker* #pw-group-fun-tools** @param string $method Requested method name* @param array $arguments Arguments provided to the method* @return null|mixed* @throws WireException**/protected function ___callUnknown($method, $arguments) {if(!isset($arguments[0])) {// explode the property to an arrayreturn $this->explode($method);} else if(is_string($arguments[0])) {// implode the property identified by $method and glued by $arguments[0]// with optional $options as second argument$delimiter = $arguments[0];$options = array();if(isset($arguments[1]) && is_array($arguments[1])) $options = $arguments[1];return $this->implode($delimiter, $method, $options);} else {// failreturn parent::___callUnknown($method, $arguments);}}/*** Perform an action upon each item in the WireArray** This is typically used to execute a function for each item, or to build a string* or array from each item.** ~~~~~* // Generate navigation list of page children:* echo $page->children()->each(function($child) {* return "<li><a href='$child->url'>$child->title</a></li>";* });** // If 2 arguments specified to custom function(), 1st is the key, 2nd is the value* echo $page->children()->each(function($key, $child) {* return "<li><a href='$child->url'>$key: $child->title</a></li>";* });** // Same as above using different method (template string):* echo $page->children()->each("<li><a href='{url}'>{title}</a></li>");** // If WireArray used to hold non-object items, use only {key} and/or {value}* echo $items->each('<li>{key}: {value}</li>');** // Get an array of all "title" properties* $titles = $page->children()->each("title");** // Get array of "title" and "url" properties. Returns an array* // containing an associative array for each item with "title" and "url"* $properties = $page->children()->each(["title", "url"]);* ~~~~~** #pw-group-traversal* #pw-group-output-rendering* #pw-group-fun-tools** @param callable|string|array|null $func Accepts any of the following:* 1. Callable function that each item will be passed to as first argument. If this* function returns a string, it will be appended to that of the other items and* the result returned by this each() method.* 2. Markup or text string with variable {tags} within it where each {tag} resolves* to a property in each item. This each() method will return the concatenated result.* 3. A property name (string) common to items in this WireArray. The result will be* returned as an array.* 4. An array of property names common to items in this WireArray. The result will be* returned as an array of associative arrays indexed by property name.** @return array|null|string|WireArray Returns one of the following (related to numbers above):* - `$this` (1a): WireArray if given a function that has no return values (if using option #1 in arguments).* - `string` (1b): Containing the concatenated results of all function calls, if your function* returns strings (if using option #1 in arguments).* - `string` (2): Returns the processed and concatenated result (string) of all items in your* template string (if using option #2 in arguments).* - `array` (3): Returns regular PHP array of the property values for each item you requested* (if using option #3 in arguments).* - `array` (4): Returns an array of associative arrays containing the property values for each item* you requested (if using option #4 in arguments).* @see WireArray::implode(), WireArray::explode()**/public function each($func = null) {$result = null; // return value, if it's detected that one is desiredif(is_callable($func)) {$funcInfo = new \ReflectionFunction($func);$useIndex = $funcInfo->getNumberOfParameters() > 1;foreach($this as $index => $item) {$val = $useIndex ? $func($index, $item) : $func($item);if($val && is_string($val)) {// function returned a string, so we assume they are wanting us to return the resultif(is_null($result)) $result = '';// if returned value resulted in {tags}, go ahead and parse themif(strpos($val, '{') !== false && strpos($val, '}')) {if(is_object($item)) {$val = wirePopulateStringTags($val, $item);} else {$val = wirePopulateStringTags($val, array('key' => $index, 'value' => $item));}}$result .= $val;}}} else if(is_string($func) && strpos($func, '{') !== false && strpos($func, '}')) {// string with variables$result = '';foreach($this as $key => $item) {if(is_object($item)) {$result .= wirePopulateStringTags($func, $item);} else {$result .= wirePopulateStringTags($func, array('key' => $key, 'value' => $item));}}} else {// array or string or nullif(is_null($func)) $func = 'name';$result = $this->explode($func);}return $result === null ? $this : $result;}/*** Divide this WireArray into $qty slices and return array of them (each being another WireArray)** This is not destructive to the original WireArray as it returns new WireArray objects.** #pw-group-retrieval* #pw-group-traversal** @param int $qty Number of slices* @return array Array of WireArray objects**/public function slices($qty) {$slices = array();if($qty < 1) return $slices;$total = $this->count();$limit = $total ? ceil($total / $qty) : 0;$start = 0;for($n = 0; $n < $qty; $n++) {if($start < $total) {$slice = $this->slice($start, $limit);} else {$slice = $this->makeNew();}$slices[] = $slice;$start += $limit;}return $slices;}/*** Set the current duplicate checking state** Applies only to non-associative WireArray types.** @param bool $value True to enable dup check, false to disable**/public function setDuplicateChecking($value) {if(!$this->usesNumericKeys()) return;$this->duplicateChecking = (bool) $value;}/*** debugInfo PHP 5.6+ magic method** @return array**/public function __debugInfo() {$info = array('count' => $this->count(),'items' => array(),);$info = array_merge($info, parent::__debugInfo());if(count($this->data)) {$info['items'] = array();foreach($this->data as $key => $value) {if(is_object($value) && $value instanceof Wire) $key = $value->className() . ":$key";$info['items'][$key] = $this->debugInfoItem($value);}}if(count($this->extraData)) $info['extraData'] = $this->extraData;$trackers = array('itemsAdded' => $this->itemsAdded,'itemsRemoved' => $this->itemsRemoved);foreach($trackers as $key => $value) {if(!count($value)) continue;$info[$key] = array();foreach($value as $k => $v) {$info[$key][] = $this->debugInfoItem($v);}}return $info;}/*** Return debug info for one item from this WireArray** #pw-internal** @param mixed $item* @return mixed|null|string**/public function debugInfoItem($item) {if(is_object($item)) {if($item instanceof Page) {$item = $item->debugInfoSmall();} else if($item instanceof WireData) {$_item = $item;$item = $item->get('name');if(!$item) $item = $_item->get('id');if(!$item) $item = $_item->className();} else {// keep $value as it is}}return $item;}/*** Static method caller, primarily for support of WireArray::new() method** @param string $name* @param array $arguments* @return mixed* @throws WireException**/public static function __callStatic($name, $arguments) {$class = get_called_class();if($name === 'new') {$n = count($arguments);if($n === 0) {// no items specified$items = null;} else if($n === 1) {$items = reset($arguments);if(is_array($items) || $items instanceof WireArray) {// multiple items specified in one argument} else {// one item specified$items = array($items);}} else {// multiple items specified as arguments$items = $arguments;}return self::newInstance($items, $class);} else {throw new WireException("Unrecognized static method: $class::$name()");}}/*** Create new instance of this class** Method for internal use, use `$a = WireArray::new($items)` or `$a = WireArrray($items)` instead.** #pw-internal** @param array|WireArray|null $items Items to add or omit (null) for none* @param string $class Class name to instantiate or omit for called class* @return WireArray**/public static function newInstance($items = null, $class = '') {if(empty($class)) $class = get_called_class();/** @var WireArray $a */$a = new $class();if($items instanceof WireArray) {$items->wire($a);$a->import($items);} else if(is_array($items)) {if(ctype_digit(implode('0', array_keys($items)))) {$a->import($items);} else {$a->setArray($items);}} else if($items !== null) {$a->add($items);}return $a;}}define('SORT_APPEND_NULLS', 32);