artabro/wire/core/WireData.php
2024-08-27 11:35:37 +02:00

558 lines
15 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<?php namespace ProcessWire;
/**
* ProcessWire 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;
}
}