<?php
/**
* @property string $pathSeparator
* @property-read AddressableTree|null $rootNode
*/
class AddressableTree implements ArrayAccess, RecursiveIterator
{
/**
* @var string
*/
private $pathSeparator;
/**
* @var AddressableTree
*/
private $rootNode;
/**
* @var array
*/
private $data = [];
/**
* @var self[]
*/
private $branches = [];
/**
* @param array $data
* @param string $pathSeparator
*/
public function __construct(array $data = [], $pathSeparator = '/')
{
$this->setRootNode(null);
$this->setPathSeparator($pathSeparator);
foreach ($data as $key => $value) {
$this->setElementAtKey($key, is_array($value) ? $this->createBranch($value) : $value, true);
}
}
/**
* @param string $name
* @return mixed
*/
public function __get($name)
{
if (!in_array($name, ['pathSeparator', 'rootNode'])) {
throw new \LogicException('Read of undefined property: ' . $name);
}
return $this->$name;
}
/**
* @param string $name
* @param mixed $value
*/
public function __set($name, $value)
{
if (!in_array($name, ['pathSeparator', 'rootNode'])) {
throw new \LogicException('Write of undefined property: ' . $name);
}
call_user_func([$this, 'set' . $name], $value);
}
/**
* @param string $value
*/
private function setPathSeparator($value)
{
$value = (string)$value;
if ($this->rootNode && $value !== $this->rootNode->pathSeparator) {
throw new \LogicException('Path separator can only be set on the root node');
}
$this->pathSeparator = $value;
foreach ($this->branches as $branch) {
$branch->pathSeparator = $value;
}
}
/**
* @param AddressableTree|null $value
*/
private function setRootNode(AddressableTree $value = null)
{
$this->rootNode = $value;
$branchRoot = $value ?: $this;
foreach ($this->branches as $branch) {
$branch->rootNode = $branchRoot;
}
}
/**
* @param array $value
* @return AddressableTree
*/
private function createBranch(array $value)
{
$branch = new self($value, $this->pathSeparator);
$branch->rootNode = $this->rootNode ?: $this;
return $branch;
}
/**
* @param $address
* @return array|bool
*/
private function parseAddressParts($address)
{
if (false === $pos = strpos($address, $this->pathSeparator)) {
return false;
} else if (!$pos && !$pos = strpos($address, $this->pathSeparator, strlen($this->pathSeparator))) {
return false;
}
return [substr($address, 0, $pos), substr($address, $pos + strlen($this->pathSeparator))];
}
/**
* @param string $key
* @param mixed $value
* @param bool $forceBranch
*/
private function setElementAtKey($key, $value, $forceBranch = false)
{
if ($value instanceof self) {
if ($value->rootNode && !$forceBranch) {
throw new \LogicException('Cannot add branch to tree: already attached to a tree');
}
$value->rootNode = $this->rootNode;
$value->pathSeparator = $this->pathSeparator;
if (isset($this->data[$key])) {
$this->removeElementAtKey($key);
}
$this->data[$key] = $value;
$this->branches[$key] = $value;
} else {
$this->data[$key] = $value;
}
}
/**
* @param string $key
*/
private function removeElementAtKey($key)
{
if (!array_key_exists($key, $this->data)) {
throw new \LogicException("Cannot remove element at '$key': does not exist");
}
if ($this->data[$key] instanceof self) {
$this->data[$key]->rootNode = null;
unset($this->branches[$key]);
}
unset($this->data[$key]);
}
/**
* @param string $address
* @param mixed $value
*/
public function setElementAtAddress($address, $value)
{
if (!$parts = $this->parseAddressParts($address)) {
$this->setElementAtKey($address, $value);
return;
}
list($key, $subAddress) = $parts;
if (!array_key_exists($key, $this->data)) {
$this->data[$key] = $this->branches[$key] = new self([], $this->pathSeparator);
$this->branches[$key]->rootNode = $this->rootNode;
} else if (!($this->data[$key] instanceof self)) {
throw new \InvalidArgumentException("Target element address invalid: treats leaf '{$key}' as a branch");
}
$this->branches[$key]->setElementAtAddress($subAddress, $value);
}
/**
* @param string $address
* @return mixed
*/
public function getElementAtAddress($address)
{
if (!$parts = $this->parseAddressParts($address)) {
if (!array_key_exists($address, $this->data)) {
throw new \InvalidArgumentException("Target leaf address invalid: element '{$address}' does not exist");
}
return $this->data[$address];
}
list($key, $subAddress) = $parts;
if (!array_key_exists($key, $this->data)) {
throw new \InvalidArgumentException("Target element address invalid: branch '{$key}' does not exist");
} else if (!($this->data[$key] instanceof self)) {
throw new \InvalidArgumentException("Target element address invalid: treats leaf '{$key}' as a branch");
}
return $this->branches[$key]->getElementAtAddress($subAddress);
}
/**
* @param string $address
*/
public function removeElementAtAddress($address)
{
if (!$parts = $this->parseAddressParts($address)) {
$this->removeElementAtKey($address);
return;
}
list($key, $subAddress) = $parts;
if (!array_key_exists($key, $this->data)) {
throw new \InvalidArgumentException("Target element address invalid: branch '{$key}' does not exist");
} else if (!($this->data[$key] instanceof self)) {
throw new \InvalidArgumentException("Target element address invalid: treats leaf '{$key}' as a branch");
}
$this->branches[$key]->removeElementAtAddress($subAddress);
}
/**
* @param string $address
* @return bool
*/
public function addressExists($address)
{
if (!$parts = $this->parseAddressParts($address)) {
return array_key_exists($address, $this->data);
}
list($key, $subAddress) = $parts;
if (!array_key_exists($key, $this->data) || !($this->data[$key] instanceof self)) {
return false;
}
return $this->branches[$key]->addressExists($subAddress);
}
/**
* @return mixed
*/
public function current()
{
return current($this->data);
}
public function next()
{
next($this->data);
}
/**
* @return string
*/
public function key()
{
return key($this->data);
}
/**
* @return bool
*/
public function valid()
{
return key($this->data) !== null;
}
public function rewind()
{
reset($this->data);
}
/**
* @return bool
*/
public function hasChildren()
{
return current($this->data) instanceof self;
}
/**
* @return RecursiveIterator|null
*/
public function getChildren()
{
return current($this->data) instanceof self ? current($this->data) : null;
}
/**
* @param string $address
* @return bool
*/
public function offsetExists($address)
{
return $this->addressExists($address);
}
/**
* @param string $address
* @return mixed
*/
public function offsetGet($address)
{
return $this->getElementAtAddress($address);
}
/**
* @param string $address
* @param mixed $value
*/
public function offsetSet($address, $value)
{
$this->setElementAtAddress($address, $value);
}
/**
* @param string $key
*/
public function offsetUnset($key)
{
$this->removeElementAtAddress($key);
}
}
$tree = new AddressableTree([
'foo' => [
'bar' => 1,
'baz' => 2,
],
'qux' => [
'yo' => ['mama' => ['so' => 'fat']]
],
'stuff' => 'ting',
]);
var_dump($tree);
/*
var_dump($tree['foo/bar'], $tree['/foo/bar'], $tree['foo']['bar']);
$tree['/yo/mama/so'] = 'ugly';
var_dump($tree);
preferences:
44.44 ms | 402 KiB | 5 Q