558 lines
15 KiB
PHP
558 lines
15 KiB
PHP
<?php namespace ProcessWire;
|
||
|
||
/**
|
||
* ProcessWire WireData
|
||
*
|
||
* This is the base data container class used throughout ProcessWire.
|
||
* It provides get and set access to properties internally stored in a $data array.
|
||
* Otherwise it is identical to the Wire class.
|
||
*
|
||
* #pw-summary WireData is the base data-storage class used by many ProcessWire object types and most modules.
|
||
* #pw-body =
|
||
* WireData is very much like its parent `Wire` class with the fundamental difference being that it is designed
|
||
* for runtime data storage. It provides this primarily through the built-in `get()` and `set()` methods for
|
||
* getting and setting named properties to WireData objects. The most common example of a WireData object is
|
||
* `Page`, the type used for all pages in ProcessWire.
|
||
*
|
||
* Properties set to a WireData object can also be set or accessed directly, like `$item->property` or using
|
||
* array access like `$item[$property]`. If you `foreach()` a WireData object, the default behavior is to
|
||
* iterate all of the properties/values present within it.
|
||
* #pw-body
|
||
*
|
||
* May also be accessed as array.
|
||
*
|
||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||
* https://processwire.com
|
||
*
|
||
* @method WireArray and($items = null)
|
||
*
|
||
*/
|
||
|
||
class WireData extends Wire implements \IteratorAggregate, \ArrayAccess {
|
||
|
||
/**
|
||
* Array where get/set properties are stored
|
||
*
|
||
*/
|
||
protected $data = array();
|
||
|
||
/**
|
||
* Set a value to this object’s data
|
||
*
|
||
* ~~~~~
|
||
* // Set a value for a property
|
||
* $item->set('foo', 'bar');
|
||
*
|
||
* // Set a property value directly
|
||
* $item->foo = 'bar';
|
||
*
|
||
* // Set a property using array access
|
||
* $item['foo'] = 'bar';
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-manipulation
|
||
*
|
||
* @param string $key Name of property you want to set
|
||
* @param mixed $value Value of property
|
||
* @return $this
|
||
* @see WireData::setQuietly(), WireData::get()
|
||
*
|
||
*/
|
||
public function set($key, $value) {
|
||
if($key === 'data') {
|
||
if(!is_array($value)) $value = (array) $value;
|
||
return $this->setArray($value);
|
||
}
|
||
if($this->trackChanges) {
|
||
$v = isset($this->data[$key]) ? $this->data[$key] : null;
|
||
if(!$this->isEqual($key, $v, $value)) $this->trackChange($key, $v, $value);
|
||
}
|
||
$this->data[$key] = $value;
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Same as set() but without change tracking
|
||
*
|
||
* - If `$this->trackChanges()` is false, then this is no different than set(), since changes aren't being tracked.
|
||
* - If `$this->trackChanges()` is true, then the value will be set quietly (i.e. not recorded in the changes list).
|
||
*
|
||
* #pw-group-manipulation
|
||
*
|
||
* @param string $key Name of property you want to set
|
||
* @param mixed $value Value of property
|
||
* @return $this
|
||
* @see Wire::trackChanges(), WireData::set()
|
||
*
|
||
*/
|
||
public function setQuietly($key, $value) {
|
||
$track = $this->trackChanges();
|
||
if($track) $this->setTrackChanges(false);
|
||
$this->set($key, $value);
|
||
if($track) $this->setTrackChanges(true);
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Is $value1 equal to $value2?
|
||
*
|
||
* This template method provided so that descending classes can optionally determine
|
||
* whether a change should be tracked.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $key Name of the property/key that triggered the check (see `WireData::set()`)
|
||
* @param mixed $value1 Comparison value
|
||
* @param mixed $value2 A second comparison value
|
||
* @return bool True if values are equal, false if not
|
||
*
|
||
*/
|
||
protected function isEqual($key, $value1, $value2) {
|
||
if($key) {} // intentional to avoid unused argument notice
|
||
// $key intentionally not used here, but may be used by descending classes
|
||
return $value1 === $value2;
|
||
}
|
||
|
||
/**
|
||
* Set an array of key=value pairs
|
||
*
|
||
* This is the same as the `WireData::set()` method except that it can set an array
|
||
* of properties at once.
|
||
*
|
||
* #pw-group-manipulation
|
||
*
|
||
* @param array $data Associative array of where the keys are property names, and values are… values.
|
||
* @return $this
|
||
* @see WireData::set()
|
||
*
|
||
*/
|
||
public function setArray(array $data) {
|
||
foreach($data as $key => $value) $this->set($key, $value);
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Provides direct reference access to set values in the $data array
|
||
*
|
||
* @param string $key
|
||
* @param mixed $value
|
||
*
|
||
*/
|
||
public function __set($key, $value) {
|
||
$this->set($key, $value);
|
||
}
|
||
|
||
/**
|
||
* Retrieve the value for a previously set property, or retrieve an API variable
|
||
*
|
||
* - If the given $key is an object, it will cast it to a string.
|
||
* - If the given $key is a string with "|" pipe characters in it, it will try all till it finds a non-empty value.
|
||
* - If given an API variable name, it will return that API variable unless the class has direct access API variables disabled.
|
||
*
|
||
* ~~~~~
|
||
* // Retrieve the value of a property
|
||
* $value = $item->get("some_property");
|
||
*
|
||
* // Retrieve the value of the first non-empty property:
|
||
* $value = $item->get("property1|property2|property2");
|
||
*
|
||
* // Retrieve a value using array access
|
||
* $value = $item["some_property"];
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-retrieval
|
||
*
|
||
* @param string|object $key Name of property you want to retrieve.
|
||
* @return mixed|null Returns value of requested property, or null if the property was not found.
|
||
* @see WireData::set()
|
||
*
|
||
*/
|
||
public function get($key) {
|
||
if(is_object($key)) $key = "$key";
|
||
if(array_key_exists($key, $this->data)) return $this->data[$key];
|
||
if(strpos($key, '|')) {
|
||
$keys = explode('|', $key);
|
||
foreach($keys as $k) {
|
||
/** @noinspection PhpAssignmentInConditionInspection */
|
||
if($value = $this->get($k)) return $value;
|
||
}
|
||
}
|
||
return parent::__get($key); // back to Wire
|
||
}
|
||
|
||
/**
|
||
* Get or set a low-level data value
|
||
*
|
||
* Like get() or set() but will only get/set from the WireData's protected $data array.
|
||
* This is used to bypass any extra logic a class may have added to its get() or set()
|
||
* methods. The benefit of this method over get() is that it excludes API vars and potentially
|
||
* other things (defined by descending classes) that you may not want.
|
||
*
|
||
* - To get a value, simply omit the $value argument.
|
||
* - To set a value, specify both the $key and $value arguments.
|
||
* - If you omit a $key and $value, this method will return the entire data array.
|
||
*
|
||
* #pw-group-manipulation
|
||
* #pw-group-retrieval
|
||
*
|
||
* ~~~~~
|
||
* // Set a property
|
||
* $item->data('some_property', 'some value');
|
||
*
|
||
* // Get the value of a previously set property
|
||
* $value = $item->data('some_property');
|
||
* ~~~~~
|
||
*
|
||
* @param string|array $key Property you want to get or set, or associative array of properties you want to set.
|
||
* @param mixed $value Optionally specify a value if you want to set rather than get.
|
||
* Or Specify boolean TRUE if setting an array via $key and you want to overwrite any existing values (rather than merge).
|
||
* @return array|WireData|null Returns one of the following:
|
||
* - `mixed` - Actual value if getting a previously set value.
|
||
* - `null` - If you are attempting to get a value that has not been set.
|
||
* - `$this` - If you are setting a value.
|
||
*/
|
||
public function data($key = null, $value = null) {
|
||
if($key === null) return $this->data;
|
||
if(is_array($key)) {
|
||
if($value === true) {
|
||
$this->data = $key;
|
||
} else {
|
||
$this->data = array_merge($this->data, $key);
|
||
}
|
||
return $this;
|
||
} else if($value === null) {
|
||
return isset($this->data[$key]) ? $this->data[$key] : null;
|
||
} else {
|
||
$this->data[$key] = $value;
|
||
return $this;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns the full array of properties set to this object
|
||
*
|
||
* If descending classes also store data in other containers, they may want to
|
||
* override this method to include that data as well.
|
||
*
|
||
* #pw-group-retrieval
|
||
*
|
||
* @return array Returned array is associative and indexed by property name.
|
||
*
|
||
*/
|
||
public function getArray() {
|
||
return $this->data;
|
||
}
|
||
|
||
/**
|
||
* Get a property via dot syntax: field.subfield (static)
|
||
*
|
||
* Static version for internal core use. Use the non-static getDot() instead.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $key
|
||
* @param Wire $from The instance you want to pull the value from
|
||
* @return null|mixed Returns value if found or null if not
|
||
*
|
||
*/
|
||
public static function _getDot($key, Wire $from) {
|
||
$key = trim($key, '.');
|
||
if(strpos($key, '.')) {
|
||
// dot present
|
||
$keys = explode('.', $key); // convert to array
|
||
$key = array_shift($keys); // get first item
|
||
} else {
|
||
// dot not present
|
||
$keys = array();
|
||
}
|
||
if($from->wire($key) !== null) return null; // don't allow API vars to be retrieved this way
|
||
if($from instanceof WireData) {
|
||
$value = $from->get($key);
|
||
} else if($from instanceof WireArray) {
|
||
$value = $from->getProperty($key);
|
||
} else {
|
||
$value = $from->$key;
|
||
}
|
||
if(!count($keys)) return $value; // final value
|
||
if(is_object($value)) {
|
||
if(count($keys) > 1) {
|
||
$keys = implode('.', $keys); // convert back to string
|
||
if($value instanceof WireData) $value = $value->getDot($keys); // for override potential
|
||
else $value = self::_getDot($keys, $value);
|
||
} else {
|
||
$key = array_shift($keys);
|
||
// just one key left, like 'title'
|
||
if($value instanceof WireData) {
|
||
$value = $value->get($key);
|
||
} else if($value instanceof WireArray) {
|
||
if($key == 'count') {
|
||
$value = count($value);
|
||
} else {
|
||
$a = array();
|
||
foreach($value as $v) $a[] = $v->get($key);
|
||
$value = $a;
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// there is a dot property remaining and nothing to send it to
|
||
$value = null;
|
||
}
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* Get a property via dot syntax: field.subfield.subfield
|
||
*
|
||
* Some classes descending WireData may choose to add a call to this as part of their
|
||
* get() method as a syntax convenience.
|
||
*
|
||
* ~~~~~
|
||
* $value = $item->get("parent.title");
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-retrieval
|
||
*
|
||
* @param string $key Name of property you want to retrieve in "a.b" or "a.b.c" format
|
||
* @return null|mixed Returns value if found or null if not
|
||
*
|
||
*/
|
||
public function getDot($key) {
|
||
return self::_getDot($key, $this);
|
||
}
|
||
|
||
/**
|
||
* Provides direct reference access to variables in the $data array
|
||
*
|
||
* Otherwise the same as get()
|
||
*
|
||
* @param string $name
|
||
* @return mixed|null
|
||
*
|
||
*/
|
||
public function __get($name) {
|
||
return $this->get($name);
|
||
}
|
||
|
||
/**
|
||
* Enables use of $var('key')
|
||
*
|
||
* @param string $key
|
||
* @return mixed
|
||
*
|
||
*/
|
||
public function __invoke($key) {
|
||
return $this->get($key);
|
||
}
|
||
|
||
/**
|
||
* Remove a previously set property
|
||
*
|
||
* ~~~~~
|
||
* $item->remove('some_property');
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-manipulation
|
||
*
|
||
* @param string $key Name of property you want to remove
|
||
* @return $this
|
||
*
|
||
*/
|
||
public function remove($key) {
|
||
$value = isset($this->data[$key]) ? $this->data[$key] : null;
|
||
$this->trackChange("unset:$key", $value, null);
|
||
unset($this->data[$key]);
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Enables the object data properties to be iterable as an array
|
||
*
|
||
* ~~~~~
|
||
* foreach($item as $key => $value) {
|
||
* // ...
|
||
* }
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-retrieval
|
||
*
|
||
* @return \ArrayObject
|
||
*
|
||
*/
|
||
#[\ReturnTypeWillChange]
|
||
public function getIterator() {
|
||
return new \ArrayObject($this->data);
|
||
}
|
||
|
||
/**
|
||
* Does this object have the given property?
|
||
*
|
||
* ~~~~~
|
||
* if($item->has('some_property')) {
|
||
* // the item has some_property
|
||
* }
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-retrieval
|
||
*
|
||
* @param string $key Name of property you want to check.
|
||
* @return bool True if it has the property, false if not.
|
||
*
|
||
*/
|
||
public function has($key) {
|
||
if(isset($this->data[$key])) return true; // optimization
|
||
return ($this->get($key) !== null);
|
||
}
|
||
|
||
/**
|
||
* Take the current item and append the given item(s), returning a new WireArray
|
||
*
|
||
* This is for syntactic convenience in fluent interfaces.
|
||
* ~~~~~
|
||
* if($page->and($page->parents)->has("featured=1")) {
|
||
* // page or one of its parents has a featured property with value of 1
|
||
* }
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-retrieval
|
||
*
|
||
* @param WireArray|WireData|string|null $items May be any of the following:
|
||
* - `WireData` object (or derivative)
|
||
* - `WireArray` object (or derivative)
|
||
* - Name of any property from this object that returns one of the above.
|
||
* - Omit argument to simply return this object in a WireArray
|
||
* @return WireArray Returns a WireArray of this object *and* the one(s) given.
|
||
* @throws WireException If invalid argument supplied.
|
||
*
|
||
*/
|
||
public function ___and($items = null) {
|
||
|
||
if(is_string($items)) $items = $this->get($items);
|
||
|
||
if($items instanceof WireArray) {
|
||
// great, that's what we want
|
||
$a = clone $items;
|
||
$a->prepend($this);
|
||
} else if($items instanceof WireData || is_null($items)) {
|
||
// single item
|
||
$className = $this->className(true) . 'Array';
|
||
if(!class_exists($className)) $className = wireClassName('WireArray', true);
|
||
/** @var WireArray $a */
|
||
$a = $this->wire(new $className());
|
||
$a->add($this);
|
||
if($items) $a->add($items);
|
||
} else {
|
||
// unknown
|
||
throw new WireException('Invalid argument provided to WireData::and(...)');
|
||
}
|
||
|
||
return $a;
|
||
}
|
||
|
||
/**
|
||
* Ensures that isset() and empty() work for this classes properties.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $key
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function __isset($key) {
|
||
return isset($this->data[$key]);
|
||
}
|
||
|
||
/**
|
||
* Ensures that unset() works for this classes data.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $key
|
||
*
|
||
*/
|
||
public function __unset($key) {
|
||
$this->remove($key);
|
||
}
|
||
|
||
/**
|
||
* Sets an index in the WireArray.
|
||
*
|
||
* For the ArrayAccess interface.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param int|string $offset Key of item to set.
|
||
* @param int|string|array|object $value Value of item.
|
||
*
|
||
*/
|
||
#[\ReturnTypeWillChange]
|
||
public function offsetSet($offset, $value) {
|
||
$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 int|string|array|object Value of item requested, or false if it doesn't exist.
|
||
*
|
||
*/
|
||
#[\ReturnTypeWillChange]
|
||
public function offsetGet($offset) {
|
||
$value = $this->get($offset);
|
||
return is_null($value) ? false : $value;
|
||
}
|
||
|
||
/**
|
||
* 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->__isset($offset)) {
|
||
$this->remove($offset);
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Determines if the given index exists in this WireData.
|
||
*
|
||
* For the ArrayAccess interface.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param int|string $offset Key of the item to check for existence.
|
||
* @return bool True if the item exists, false if not.
|
||
*
|
||
*/
|
||
#[\ReturnTypeWillChange]
|
||
public function offsetExists($offset) {
|
||
return $this->__isset($offset);
|
||
}
|
||
|
||
/**
|
||
* debugInfo PHP 5.6+ magic method
|
||
*
|
||
* @return array
|
||
*
|
||
*/
|
||
public function __debugInfo() {
|
||
$info = parent::__debugInfo();
|
||
if(count($this->data)) $info['data'] = $this->data;
|
||
return $info;
|
||
}
|
||
|
||
}
|
||
|