artabro/wire/core/WireData.php

559 lines
15 KiB
PHP
Raw Permalink Normal View History

2024-08-27 11:35:37 +02:00
<?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 objects 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;
}
}