2735 lines
77 KiB
PHP
2735 lines
77 KiB
PHP
<?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 (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) {
|
||
|
||
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 there’s 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 isn’t 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);
|