plus-8.x-4.x-dev/src/Utility/ArrayObject.php
src/Utility/ArrayObject.php
<?php
namespace Drupal\plus\Utility;
/**
* Further extends SPL ArrayObject with a little more functionality.
*
* @ingroup utility
*/
class ArrayObject extends \ArrayObject implements ArrayObjectInterface {
// The following properties are purposefully prefixed with __ so they don't
// clash with any stored property names. To ensure tests pass, Drupal coding
// standards must be ignored here.
// @codingStandardsIgnoreStart
/**
* Flag indicating whether the object has changed.
*
* @var bool
*/
protected $__changed = FALSE;
/**
* The currently set flag(s).
*
* @var int
*/
protected $__flags;
/**
* Flag indicating whether the object is an associative array.
*
* @var bool
*/
protected $__isAssociative;
/**
* Flag indicating whether the object is an indexed array.
*
* @var bool
*/
protected $__isIndexed;
/**
* Flag indicating whether the object is a sequentially indexed array.
*
* @var bool
*/
protected $__isSequential;
/**
* The iterator class.
*
* @var string
*/
protected $__iteratorClass;
/**
* These protected properties.
*
* @var array
*/
protected $__protectedProperties;
/**
* The internal storage of the object.
*
* @var array|mixed
*/
protected $__storage;
// @codingStandardsIgnoreEnd
/**
* {@inheritdoc}
*/
public function __construct(&$values = [], $flags = self::STD_PROP_LIST | self::ARRAY_AS_PROPS, $iterator_class = '\ArrayIterator') {
$this->setFlags($flags);
$this->setIteratorClass($iterator_class);
$this->__protectedProperties = array_keys(get_object_vars($this));
// Convert anything other than a simple array.
$values =& $this->convertArgument($values);
$merge = [];
if (is_array($values) || $values instanceof \Traversable) {
// In order to properly set objects, they must be passed through the
// ::merge method will will ultimately invoke ::convertValue which may
// be subclassed. Unfortunately, to do this and not break any references,
// that requires each of values to be temporarily moved to a different
// array (by reference) and then merged back in after the original array
// has be set.
foreach ($values as $key => &$value) {
$merge[$key] =& $value;
unset($values[$key]);
}
// In cases where this may be an indexed array, the actual index is not
// reset after each unset() call above. Some solutions point to using
// array_values() to reset this index, but that would also destroy any
// passed reference. Instead, use array_splice() which keeps any reference
// and resets the index. This is safe to use here because all the values
// were just moved to the temporary $merge array.
array_splice($values, 0, 1);
}
// It is now safe to reference the original values array.
$this->__storage =& $values;
// Merge in any original values.
if ($merge) {
$this->merge($merge);
}
// Reset changed status.
$this->changed(FALSE);
}
/**
* {@inheritdoc}
*/
public static function create(...$arguments) {
// Only clone the first object.
if (isset($arguments[0])) {
$values = $arguments[0];
if (is_object($values)) {
$values = clone $values;
}
$arguments[0] = $values;
}
return static::reference(...$arguments);
}
/**
* {@inheritdoc}
*/
public static function reference(&...$arguments) {
// Convert any StaticArray instances into a normal array (by reference) so
// a new object instance can be created. This is needed to allow any
// additional parameters (if subclassed) to be sent to the constructor,
// which gives the illusion of "updating" references as needed.
if (isset($arguments[0])) {
if ($arguments[0] instanceof static) {
$arguments[0] =& $arguments[0]->value();
}
}
return new static(...$arguments);
}
/**
* {@inheritdoc}
*/
public function &__get($key) {
$ret = NULL;
if ($this->__flags == self::ARRAY_AS_PROPS) {
$ret =& $this->offsetGet($key);
return $ret;
}
if (in_array($key, $this->__protectedProperties)) {
throw new \InvalidArgumentException('$key is a protected property, use a different key');
}
return $this->$key;
}
/**
* {@inheritdoc}
*/
public function __isset($key) {
if ($this->__flags == self::ARRAY_AS_PROPS) {
return $this->offsetExists($key);
}
if (in_array($key, $this->__protectedProperties)) {
throw new \InvalidArgumentException('$key is a protected property, use a different key');
}
return isset($this->$key);
}
/**
* {@inheritdoc}
*/
public function __set($key, $value) {
$value = $this->convertValue($key, $value);
if ($this->__flags == self::ARRAY_AS_PROPS) {
$this->offsetSet($key, $value);
}
if (in_array($key, $this->__protectedProperties)) {
throw new \InvalidArgumentException('$key is a protected property, use a different key');
}
$this->$key = $value;
}
/**
* {@inheritdoc}
*/
public function __unset($key) {
if ($this->__flags == self::ARRAY_AS_PROPS) {
$this->offsetUnset($key);
}
if (in_array($key, $this->__protectedProperties)) {
throw new \InvalidArgumentException('$key is a protected property, use a different key');
}
unset($this->$key);
}
/**
* {@inheritdoc}
*/
public function append($value, $key = NULL) {
$original = $this->__storage;
$value = $this->convertValue($key, $value);
$this->__storage += isset($key) ? [$key => $value] : [$value];
if (!$this->equals($original)) {
$this->changed();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function asort() {
if (is_array($this->__storage)) {
asort($this->__storage);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function count($mode = COUNT_NORMAL) {
if (is_array($this->__storage) || is_object($this->__storage)) {
return count($this->__storage, $mode);
}
elseif (is_string($this->__storage)) {
return strlen($this->__storage);
}
return (int) !!$this->__storage;
}
/**
* Base flatten.
*
* @param array $array
* The array to iterate over.
* @param bool $shallow
* Flag indicating whether the array will only flatten a single level.
* @param bool $strict
* Flag indicating whether to only flatten arrays.
*
* @return array
* The flattened array.
*
* @see https://github.com/tajawal/lodash-php/blob/master/src/arrays/flatten.php
*/
protected function baseFlatten(array $array, $shallow = FALSE, $strict = TRUE) {
$output = [];
$idx = 0;
foreach ($array as $index => $value) {
if (is_array($value)) {
if (!$shallow) {
$value = $this->baseFlatten($value, $shallow, $strict);
}
$j = 0;
$len = count($value);
while ($j < $len) {
$output[$idx++] = $value[$j++];
}
}
else {
if (!$strict) {
$output[$idx++] = $value;
}
}
}
return $output;
}
/**
* Indicates when the array has changed.
*
* @param bool $changed
* Flag indicating whether value has changed.
*
* @return static
*/
protected function changed($changed = TRUE) {
$this->__changed = $changed;
// Reset any cached values on the type of array.
unset($this->__isAssociative, $this->__isSequential);
return $this;
}
/**
* {@inheritdoc}
*/
public function copy() {
return static::create($this->__storage);
}
/**
* Converts an argument into a simple array.
*
* @param array|object|StaticArray $argument
* The argument to convert.
*
* @return array
* The converted argument.
*
* @throws \InvalidArgumentException
* When argument cannot be converted.
*/
protected function &convertArgument(&$argument) {
// Immediately return if argument is empty.
if (empty($argument)) {
return $argument;
}
// Convert objects into an StaticArray.
if (is_object($argument) && !($argument instanceof \ArrayObject)) {
$argument = (new \ArrayObject($argument))->getArrayCopy();
}
// Convert StaticArray to array.
while ($argument instanceof \ArrayObject) {
$argument = $argument->getArrayCopy();
}
return $argument;
}
/**
* Converts multiple arguments into an array of simple arrays.
*
* @param array $arguments
* An array of arguments.
*
* @return array
* The converted array.
*/
protected function &convertArguments(array &$arguments = []) {
// Iterate over the arguments and add it to the converted return array.
foreach ($arguments as &$argument) {
$array[] = &$this->convertArgument($argument);
}
return $arguments;
}
/**
* Converts a value before storing it.
*
* @param string $key
* The name of the value being stored.
* @param mixed $value
* The value to convert, passed by reference.
*
* @return mixed
* The converted value.
*/
protected function convertValue(/* @noinspection PhpUnusedParameterInspection */ $key, $value = NULL) {
return $value;
}
/**
* {@inheritdoc}
*/
public function equals($value) {
return $this->__storage === static::create($value)->value();
}
/**
* {@inheritdoc}
*
* @deprecated Use ::replace() instead.
*/
public function exchangeArray($input) {
$this->replace($input);
}
/**
* {@inheritdoc}
*/
public function exists($key, $check_key = TRUE) {
if ($this->isSequential()) {
$key = array_search($key, $this->__storage);
return $key !== FALSE;
}
return $check_key ? array_key_exists($key, $this->__storage) : $this->offsetExists($key);
}
/**
* {@inheritdoc}
*/
public function find($key, $default = NULL) {
if (@preg_match('/^\/[\s\S]+\/$/', $key)) {
return $this->findFirst($key, $default);
}
// Last ditch effort.
return $this->get($key, $default, FALSE);
}
/**
* {@inheritdoc}
*/
public function &findAll($pattern) {
$found = [];
foreach ($this->__storage as $key => $value) {
$item = $this->isSequential() ? $value : $key;
if (is_string($item) && @preg_match($pattern, $item)) {
$found[$key] = $this->get($item);
}
}
return $found;
}
/**
* {@inheritdoc}
*/
public function findFirst($pattern, $default = NULL) {
$results = $this->findAll($pattern);
return $results ? reset($results) : $default;
}
/**
* {@inheritdoc}
*/
public function findLast($pattern, $default = NULL) {
$results = $this->findAll($pattern);
return $results ? end($results) : $default;
}
/**
* {@inheritdoc}
*/
public function filter($callback = NULL, $flag = 0) {
$original = $this->value();
// Immediately return if value isn't an array.
if (!is_array($original)) {
return $this;
}
if (!isset($callback)) {
$callback = function ($val) {
return (bool) $val;
};
}
$this->__storage = array_filter($original, $callback, $flag);
if (!$this->equals($original)) {
$this->changed();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function flatten($shallow = FALSE) {
$original = $this->value();
// Immediately return if value isn't an array.
if (!is_array($original)) {
return $this;
}
$this->__storage = $this->baseFlatten($original, $shallow, FALSE);
if (!$this->equals($original)) {
$this->changed();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function &get($key, $default = NULL, $set_if_no_existence = TRUE) {
if (!$this->exists($key)) {
if (!$set_if_no_existence) {
return $default;
}
$this->set($key, $default);
}
$ret = &$this->offsetGet($key);
return $ret;
}
/**
* {@inheritdoc}
*/
public function getArrayCopy() {
$storage = $this->__storage;
if (is_array($storage)) {
return $storage;
}
elseif (is_object($storage)) {
return (array) $storage;
}
return [];
}
/**
* {@inheritdoc}
*/
public function getFlags() {
return $this->__flags;
}
/**
* {@inheritdoc}
*/
public function getIterator() {
$class = $this->getIteratorClass();
return new $class($this->__storage);
}
/**
* {@inheritdoc}
*/
public function getIteratorClass() {
return $this->__iteratorClass;
}
/**
* {@inheritdoc}
*/
public function hasChanged() {
return $this->__changed;
}
/**
* {@inheritdoc}
*/
public function isAssociative() {
if (!isset($this->__isAssociative)) {
// Explicitly set to FALSE if not an array or an empty array.
if (!is_array($this->__storage) || $this->__storage === []) {
$this->__isAssociative = FALSE;
}
// Otherwise, determine by checking if the keys are strings.
else {
$this->__isAssociative = count(array_filter(array_keys($this->__storage), 'is_string'));
}
}
return $this->__isAssociative;
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
return empty($this->__storage);
}
/**
* {@inheritdoc}
*/
public function isIndexed() {
if (!isset($this->__isIndexed)) {
// Explicitly set to FALSE if not an array.
if (!is_array($this->__storage)) {
$this->__isIndexed = FALSE;
}
// Explicitly set to TRUE if the array is empty.
elseif ($this->__storage === []) {
$this->__isIndexed = TRUE;
}
// Otherwise, determine by checking if the keys are numeric.
else {
$this->__isIndexed = count(array_filter(array_keys($this->__storage), 'is_numeric'));
}
}
return $this->__isIndexed;
}
/**
* {@inheritdoc}
*/
public function isSequential() {
if (!isset($this->__isSequential)) {
// Explicitly set to FALSE if not an array.
if (!is_array($this->__storage)) {
$this->__isSequential = FALSE;
}
// Explicitly set to TRUE if the array is empty.
elseif ($this->__storage === []) {
$this->__isSequential = TRUE;
}
// Otherwise, determine by checking if the keys match a range.
else {
$this->__isSequential = array_keys($this->__storage) === range(0, count($this->__storage) - 1);
}
}
return $this->__isSequential;
}
/**
* {@inheritdoc}
*/
public function jsonSerialize() {
return $this->getArrayCopy();
}
/**
* {@inheritdoc}
*/
public function ksort() {
if (is_array($this->__storage)) {
ksort($this->__storage);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function keys() {
return array_keys($this->__storage);
}
/**
* {@inheritdoc}
*/
public function map(callable $callback) {
$value = &$this->value();
$original = $value;
// Immediately return if value isn't an array.
if (!is_array($value)) {
return $this;
}
foreach ($value as $k => &$v) {
$value[$k] = call_user_func_array($callback, [&$v, $k, $value]);
}
$this->__storage = &$value;
if (!$this->equals($original)) {
$this->changed();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function merge(&...$arguments) {
if (is_array($this->__storage)) {
$original = $this->__storage;
$this->mergeByReference($this->__storage, $this->convertArguments($arguments));
if (!$this->equals($original)) {
$this->changed();
}
}
return $this;
}
/**
* Merges arguments onto an array passed by reference.
*
* @param array $array
* The array to merge arguments onto, passed by reference.
* @param array $arguments
* The arguments to merge onto $array, passed by reference.
* @param bool $recurse
* (optional) Flag indicating whether to recursively merge. Defaults to
* FALSE.
* @param bool $preserve_integer_keys
* (optional) Flag indicating whether integer keys will be preserved and
* merged instead of appended. Defaults to FALSE.
*/
protected function mergeByReference(array &$array = [], array &$arguments = [], $recurse = FALSE, $preserve_integer_keys = FALSE) {
foreach ($arguments as &$argument) {
// Skip any non-traversable objects.
if (!is_array($argument) && !($argument instanceof \Traversable)) {
continue;
}
foreach ($argument as $key => &$value) {
// Renumber integer keys as array_merge_recursive() does unless
// $preserve_integer_keys is set to TRUE. Note that PHP automatically
// converts keys that are integer strings (e.g. '1') to integers.
if (is_int($key) && !$preserve_integer_keys) {
$array[] = $this->convertValue($key, $value);
}
// Recurse when both values are arrays.
elseif ($recurse && isset($array[$key]) && is_array($array[$key]) && is_array($value)) {
$array[$key] = $this->mergeByReference($array[$key], $value, $recurse);
}
// Otherwise, use the latter value, overriding any previous value.
else {
$array[$key] = $this->convertValue($key, $value);
}
}
}
}
/**
* {@inheritdoc}
*/
public function mergeDeep(&...$arguments) {
if (is_array($this->__storage)) {
$original = $this->__storage;
$this->mergeByReference($this->__storage, $this->convertArguments($arguments), TRUE);
if (!$this->equals($original)) {
$this->changed();
}
}
return $this;
}
/**
* {@inheritdoc}
*/
public function natcasesort() {
if (is_array($this->__storage)) {
natcasesort($this->__storage);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function natsort() {
if (is_array($this->__storage)) {
natsort($this->__storage);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function offsetExists($key) {
return isset($this->__storage[$key]);
}
/**
* {@inheritdoc}
*/
public function &offsetGet($key) {
$ret = NULL;
if (!$this->offsetExists($key)) {
return $ret;
}
$ret =& $this->__storage[$key];
return $ret;
}
/**
* {@inheritdoc}
*/
public function offsetSet($key, $value) {
$original = $this->__storage;
$value = $this->convertValue($key, $value);
if (isset($key)) {
$this->__storage[$key] = $value;
}
else {
$this->__storage[] = $value;
}
if (!$this->equals($original)) {
$this->changed();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function offsetUnset($key) {
$original = $this->__storage;
// Look for an associative key to remove.
if ($this->isAssociative()) {
if ($this->offsetExists($key)) {
unset($this->__storage[$key]);
}
}
// Otherwise, it's a value that should be removed.
elseif ($this->isSequential()) {
$key = array_search($key, $this->__storage, TRUE);
if ($key !== FALSE) {
array_splice($this->__storage, $key, 1);
}
}
if (!$this->equals($original)) {
$this->changed();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function prepend(&$value, $key = NULL) {
$original = $this->__storage;
$value = $this->convertValue($key, $value);
if (isset($key)) {
$this->__storage = [$key => &$value] + $this->__storage;
}
else {
$this->__storage = [&$value] + $this->__storage;
}
if (!$this->equals($original)) {
$this->changed();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function remove(...$keys) {
$original = $this->__storage;
$keys = StaticArray::create($keys)->flatten()->value();
foreach ($keys as $key) {
$this->offsetUnset($key);
}
if (!$this->equals($original)) {
$this->changed();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function replace(array &$value = [], array &$previous = []) {
$previous = $this->__storage;
$this->__storage = $this->convertArgument($value);
if (!$this->equals($previous)) {
$this->changed(FALSE);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function serialize() {
$data = get_object_vars($this);
// Remove unnecessary properties.
unset($data['__changed'], $data['__isAssociative'], $data['__isIndexed'], $data['__isSequential'], $data['__protectedProperties']);
return serialize($data);
}
/**
* {@inheritdoc}
*/
public function set($key, $value = NULL) {
$this->offsetSet($key, $value);
return $this;
}
/**
* {@inheritdoc}
*/
public function setFlags($flags) {
$this->__flags = $flags;
return $this;
}
/**
* {@inheritdoc}
*/
public function setIteratorClass($class) {
if (is_object($class)) {
$class = get_class($class);
}
if (is_string($class) && strpos($class, '\\') !== 0) {
$class = '\\' . $class;
}
if (!class_exists($class)) {
throw new \InvalidArgumentException('The iterator class does not exist');
}
$this->__iteratorClass = $class;
return $this;
}
/**
* {@inheritdoc}
*/
public function similar($value) {
// This is intentionally using == as the check is not strict on purpose.
// @codingStandardsIgnoreStart
return $this->__storage == static::create($value)->value();
// @codingStandardsIgnoreEnd
}
/**
* {@inheritdoc}
*/
public function uasort($callback) {
if (is_array($this->__storage) && is_callable($callback)) {
uasort($this->__storage, $callback);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function uksort($callback) {
if (is_array($this->__storage) && is_callable($callback)) {
uksort($this->__storage, $callback);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function unique($sort_flags = SORT_STRING) {
$original = $this->value();
// Immediately return if value isn't an array.
if (!is_array($original)) {
return $this;
}
$this->__storage = array_unique($original, $sort_flags);
if (!$this->equals($original)) {
$this->changed();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function unserialize($serialized) {
$data = unserialize($serialized);
$this->__protectedProperties = array_keys(get_object_vars($this));
$this
->setIteratorClass($data['__iteratorClass'])
->setFlags($data['__flags'])
->replace($data['__storage']);
unset($data['__iteratorClass'], $data['__flags'], $data['__storage']);
// Set any other properties (due to flag).
foreach ($data as $key => $value) {
$this->__set($key, $value);
}
// Reset changed status.
$this->changed(FALSE);
}
/**
* {@inheritdoc}
*/
public function &value() {
return $this->__storage;
}
/**
* {@inheritdoc}
*/
public function walk(callable $callback, $recursive = FALSE, $user_data = NULL) {
$value = &$this->value();
$original = $value;
// Immediately return if value isn't an array.
if (!is_array($value)) {
return $this;
}
if ($recursive) {
array_walk_recursive($value, $callback, $user_data);
}
else {
array_walk($value, $callback, $user_data);
}
$this->__storage =& $value;
if (!$this->equals($original)) {
$this->changed();
}
return $this;
}
}
