src/ArrayContainer.php
<?php
namespace Tapestry;
use Closure;
use Iterator;
use ArrayAccess;
class ArrayContainer implements ArrayAccess, Iterator
{
/**
* Current array key pointer for foreach.
*
* @var int
*/
private $index = 0;
/**
* Data item array.
*
* @var array
*/
protected $items = [];
/**
* Nested Key Cache.
*
* @var array
*/
protected $nestedKeyCache = [];
/**
* ArrayContainer constructor.
*
* @param array $items
*/
public function __construct(array $items = [])
{
$this->items = $items;
}
/**
* Add or amend an item in the container by $key.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function set($key, $value)
{
$this->nestedKeyCache = [];
if ($this->isNestedKey($key)) {
$this->setNestedValueByKey($key, $value);
return;
}
$this->items[$key] = $value;
}
/**
* Remove an items from the container by $key.
*
* @param string $key
* @return void
*/
public function remove($key)
{
$this->removeKeyFromNestedCache($key);
if ($this->isNestedKey($key)) {
$this->removeNestedValueByKey($key);
return;
}
unset($this->items[$key]);
}
/**
* Get an item from the container by $key if it exists or else return $default.
*
* @param string $key
* @param mixed $default
*
* @return mixed
*/
public function get($key, $default = null)
{
if (! $this->has($key)) {
return $default;
}
if (! $this->isNestedKey($key)) {
return $this->items[$key];
}
return $this->getNestedValueByKey($key);
}
/**
* Returns boolean true if the $key exists in this container.
*
* @param string $key
*
* @return bool
*/
public function has($key)
{
if (! $this->isNestedKey($key)) {
return isset($this->items[$key]);
} else {
return ! is_null($this->getNestedValueByKey($key));
}
}
/**
* @toto implement feature
*
* @param array $items
*/
public function merge(array $items)
{
$this->items = $this->arrayMergeRecursive($this->items, $items);
$this->nestedKeyCache = [];
}
/**
* Return all items within the container.
*
* @return array
*/
public function all()
{
return $this->items;
}
public function count()
{
return count($this->items);
}
/**
* Recursive array merge found from stackoverflow.
*
* @link http://stackoverflow.com/a/25712428/1225977
*
* @param array $left
* @param array $right
*
* @return array
*/
private function arrayMergeRecursive(array &$left, array &$right)
{
$merged = $left;
foreach ($right as $key => $value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = $this->arrayMergeRecursive($merged[$key], $value);
} else {
if (is_numeric($key)) {
if (! in_array($value, $merged)) {
$merged[] = $value;
}
} else {
$merged[$key] = $value;
}
}
}
return $merged;
}
/**
* @param string $key
*
* @return bool
*/
private function isNestedKey($key)
{
return str_contains($key, '.');
}
private function removeNestedValueByKey($key)
{
// Bust Cache
$this->removeKeyFromNestedCache($key);
// Check to see if this is targeting an instance of ArrayContainer and pass the nested value on
$keyParts = explode('.', $key);
if ($this->get($keyParts[0]) instanceof self && $arrayContainer = $this->get($keyParts[0])) {
array_shift($keyParts);
/* @var ArrayContainer $arrayContainer */
$arrayContainer->remove(implode('.', $keyParts));
return true;
}
unset($keyParts);
$items = &$this->items;
$keyParts = explode('.', $key);
$lastKeyPart = end($keyParts);
foreach ($keyParts as $keyPart) {
if ($keyPart === $lastKeyPart) {
unset($items[$keyPart]);
break;
}
$items = &$items[$keyPart];
}
return true;
}
private function setNestedValueByKey($key, $value)
{
// Bust Cache
$this->removeKeyFromNestedCache($key);
// Check to see if this is targeting an instance of ArrayContainer and pass the nested value on
$keyParts = explode('.', $key);
if ($this->get($keyParts[0]) instanceof self && $arrayContainer = $this->get($keyParts[0])) {
array_shift($keyParts);
/* @var ArrayContainer $arrayContainer */
$arrayContainer->set(implode('.', $keyParts), $value);
return true;
}
unset($keyParts);
$items = &$this->items;
foreach (explode('.', $key) as $keyPart) {
$items = &$items[$keyPart];
}
return $items = $value;
}
/**
* @param string $key
*
* @return string|null
*/
private function getNestedValueByKey($key)
{
if (isset($this->nestedKeyCache[$key])) {
return $this->nestedKeyCache[$key];
}
// Check to see if this is targeting an instance of ArrayContainer and grab it from the nested container
$keyParts = explode('.', $key);
if ($this->get($keyParts[0]) instanceof self && $arrayContainer = $this->get($keyParts[0])) {
array_shift($keyParts);
/* @var ArrayContainer $arrayContainer */
$value = $arrayContainer->get(implode('.', $keyParts));
} else {
$value = $this->items;
foreach (explode('.', $key) as $keyPart) {
if ((! is_array($value) || ! $value instanceof self)) {
if (is_object($value) && method_exists($value, 'arrayAccessByKey')) {
if ($value = $value->arrayAccessByKey($keyPart)) {
break;
} else {
return null;
}
}
}
if (! isset($value[$keyPart])) {
return null;
}
if (is_array($value[$keyPart]) && ! array_key_exists($keyPart, $value)) {
return null;
}
$value = $value[$keyPart];
}
}
$this->nestedKeyCache[$key] = $value;
return $value;
}
private function removeKeyFromNestedCache($key)
{
if (isset($this->nestedKeyCache[$key])) {
unset($this->nestedKeyCache[$key]);
} else {
$this->nestedKeyCache = array_filter($this->nestedKeyCache, function ($arrayKey) use ($key) {
return strpos($arrayKey, $key) === false;
}, ARRAY_FILTER_USE_KEY);
}
}
/**
* Whether a offset exists.
*
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
*
* @param mixed $offset
*
* @return bool
*/
public function offsetExists($offset)
{
return $this->has($offset);
}
/**
* Offset to retrieve.
*
* @link http://php.net/manual/en/arrayaccess.offsetget.php
*
* @param mixed $offset
*
* @return mixed Can return all value types.
*/
public function offsetGet($offset)
{
return $this->get($offset);
}
/**
* Offset to set.
*
* @link http://php.net/manual/en/arrayaccess.offsetset.php
*
* @param mixed $offset
* @param mixed $value
*
* @return void
*/
public function offsetSet($offset, $value)
{
$this->set($offset, $value);
}
/**
* Offset to unset.
*
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
*
* @param mixed $offset
*
* @return void
*/
public function offsetUnset($offset)
{
$this->remove($offset);
}
/**
* Return the current element.
*
* @link http://php.net/manual/en/iterator.current.php
*
* @return mixed Can return any type.
*
* @since 5.0.0
*/
public function current()
{
$keys = array_keys($this->items);
$var = $this->items[$keys[$this->index]];
return $var;
}
/**
* Move forward to next element.
*
* @link http://php.net/manual/en/iterator.next.php
*
* @return void Any returned value is ignored.
*
* @since 5.0.0
*/
public function next()
{
$this->index++;
}
/**
* Return the key of the current element.
*
* @link http://php.net/manual/en/iterator.key.php
*
* @return mixed scalar on success, or null on failure.
*
* @since 5.0.0
*/
public function key()
{
$keys = array_keys($this->items);
$var = $keys[$this->index];
return $var;
}
/**
* Checks if current position is valid.
*
* @link http://php.net/manual/en/iterator.valid.php
*
* @return bool The return value will be casted to boolean and then evaluated.
* Returns true on success or false on failure.
*
* @since 5.0.0
*/
public function valid()
{
$keys = array_keys($this->items);
return isset($keys[$this->index]);
}
/**
* Rewind the Iterator to the first element.
*
* @link http://php.net/manual/en/iterator.rewind.php
*
* @return void Any returned value is ignored.
*
* @since 5.0.0
*/
public function rewind()
{
$this->index = 0;
}
/**
* @return array
*/
public function toArray()
{
return array_map(function ($value) {
return $value instanceof ArrayContainer ? $value->toArray() : $value;
}, $this->items);
}
/**
* Output the container as an array.
*
* @param int $options
*
* @return string
*/
public function toJson($options = 0)
{
return json_encode($this->toArray(), $options);
}
/**
* Sort through each item within the container by callback.
*
* @param Closure $callback
*
* @return $this
*/
public function sort(Closure $callback)
{
uasort($this->items, $callback);
return $this;
}
/**
* A 2D array sort, useful for when you need to sort a two dimensional array.
*
* @param Closure $callback
* @return $this
*/
public function sortMultiDimension(Closure $callback)
{
foreach ($this->items as &$sortable) {
uasort($sortable, $callback);
}
unset($sortable);
return $this;
}
/**
* Allow the filtering of items by key.
*
* @param array $filteredKeys
*/
public function filterKeys(array $filteredKeys = [])
{
$this->items = array_filter($this->items, function ($key) use ($filteredKeys) {
return ! isset($filteredKeys[$key]);
}, ARRAY_FILTER_USE_KEY);
}
/**
* Return an array of items where their key contains $query; this is a basic strpos check.
* Note: The $query is case sensitive.
*
* @param $query
* @return array
*/
public function find($query)
{
$output = [];
foreach (array_keys($this->items) as $key) {
if (strpos($key, $query) !== false) {
$output[$key] = $this->items[$key];
}
}
return $output;
}
}