praiadeseselle/wire/core/WireArray.php

2735 lines
77 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 2022 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;
parent::__construct();
}
/**
* 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 purposes
if($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 matches
return 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");
}
}
if($this->duplicateChecking && ($key = $this->getItemKey($item)) !== null) {
// avoid two copies of the same item, re-add it to the end
if(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 $existingItem
if($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 insert
continue;
} 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(); // item provides its own key
} else {
$k = $this->getItemKey($b);
}
$value = $b;
} else if($value === $b) {
if(method_exists($a, 'getItemKey')) {
$k = $a->getItemKey(); // item provides its own key
} 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 key
if(is_object($key)) {
/** @var object $key */
$key = $this->getItemKey($key);
/** @var string|int $key */
}
// if given an array of keys, return all matching items
if(is_array($key)) {
if(ctype_digit(implode('', array_keys($key)))) {
$items = array();
/** @var array $key */
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 so
if(isset($this->data[$key])) return $this->data[$key];
// check if key contains something other than numbers, letters, underscores, hyphens
if(is_string($key)) {
if(!ctype_alnum($key) && !ctype_alnum(strtr($key, '-_', 'ab'))) {
// check if key contains a selector
if(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} vars
return 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 string
if($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 $name
* @return Wire|WireData|Page|mixed|bool Value of item requested, or false if it doesn't exist.
*
*/
public function __get($name) {
$value = parent::__get($name);
if(is_null($value)) $value = $this->getProperty($name);
if(is_null($value)) $value = $this->get($name);
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($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 array
return $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 item
array_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 null;
$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 null;
$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 array
shuffle($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; // remember
if(is_int($flags)) $this->sortFlags($flags);
$result = $this->_sort($properties);
if(is_int($flags) && $flags !== $_flags) $this->sortFlags($_flags); // restore
return $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 (dont 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) {
if($item instanceof Wire) {
$key = $this->getItemPropertyValue($item, $property);
} else if(is_object($item)) {
if($property === '') {
$key = method_exists($item, '__toString') ? "$item" : wireClassName($item);
} else {
$key = $item->$property;
}
} else if(is_array($item)) {
$key = isset($item[$property]) ? $item[$property] : null;
} else {
// $property does not apply to non-object/non-array items
$key = $item;
}
// if theres a $subProperty and $key resolves to a containing type, then try to get it
if($subProperty && $key) {
if($key instanceof Wire) {
$key = $this->getItemPropertyValue($key, $subProperty);
} else if(is_object($key)) {
$key = $key->$subProperty;
} else if(is_array($key)) {
$key = isset($key[$subProperty]) ? $key[$subProperty] : null;
} else {
// no containing type, $subProperty ignored
}
}
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 collected
if($reverse) {
krsort($sortable, $sortFlags);
} else {
ksort($sortable, $sortFlags);
}
// add the items that resolved to no key to the end, as an array
if(!empty($nullable)) $sortable[] = $nullable;
// restore sorted array to lose sortable keys and restore proper keys
$a = array();
foreach($sortable as $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 $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($selectors instanceof Selectors) {
$selectors = clone $selectors;
} else {
if(!is_array($selectors) && ctype_digit("$selectors")) $selectors = "id=$selectors";
$selector = $selectors;
/** @var Selectors $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 selectors
foreach($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 properties
switch($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 remain
foreach($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) {
$v = $this->getItemPropertyValue($item, $field);
if(is_array($v)) $v = implode(' ', $this->wire()->sanitizer->flatArray($v));
$value[] = (string) $v;
}
} else {
$value = $this->getItemPropertyValue($item, $selector->field);
$value = is_array($value) ? $this->wire()->sanitizer->flatArray($value) : (string) $value;
}
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 used
if(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 $offset Key of item to set.
* @param Wire|mixed $value Value of item.
*
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value) {
if($offset === null) {
// i.e. $wireArray[] = $item;
$this->add($value);
} else {
$this->set($offset, $value);
}
}
/**
* Returns the value of the item at the given index, or false if not set.
*
* #pw-internal
*
* @param int|string $offset Key of item to retrieve.
* @return Wire|mixed|bool Value of item requested, or false if it doesn't exist.
*
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset) {
if($this->offsetExists($offset)) {
return $this->data[$offset];
} else {
return false;
}
}
/**
* Unsets the value at the given index.
*
* For the \ArrayAccess interface.
*
* #pw-internal
*
* @param int|string $offset 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($offset) {
if($this->offsetExists($offset)) {
$this->remove($offset);
return true;
} else {
return false;
}
}
/**
* Determines if the given index exists in this WireArray.
*
* For the \ArrayAccess interface.
*
* #pw-internal
*
* @param int|string $offset Key of the item to check for existance.
* @return bool True if the item exists, false if not.
*
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset) {
return array_key_exists($offset, $this->data);
}
/**
* Returns a string representation of this WireArray.
*
* @return string
*
*/
public function __toString() {
$s = '';
foreach($this as $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 isnt already wired
if($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 blank
if(is_array($property)) $options = $property;
$property = $delimiter;
$delimiter = '';
} else if($itemIsObject && (empty($property) || is_array($property))) {
// delimiter was omitted, forcing $property to be first arg
if(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) {
/** @var WireData $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;
}
/** @var WireData $item */
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 properties
return $this->extraData;
} else if(is_array($key)) {
// get or set multiple properties
if($value === true) {
// replace all data with data in given $key array
$this->extraData = $key;
} else {
// test if array is associative
if(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 property
unset($this->extraData[$value]);
} else if($value === null) {
// get a property
return 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 them
return $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 array
return $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 {
// fail
return 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 desired
if(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 result
if(is_null($result)) $result = '';
// if returned value resulted in {tags}, go ahead and parse them
if(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 null
if(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($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 $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 WireArray
* @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);