You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
361 lines
6.0 KiB
361 lines
6.0 KiB
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Dom;
|
|
|
|
/**
|
|
* Node in a document.
|
|
*
|
|
* @author Adam Pippin <hello@adampippin.ca>
|
|
*/
|
|
class Node implements \ArrayAccess, \Iterator
|
|
{
|
|
/**
|
|
* Index for Iterator implementation.
|
|
* @var int
|
|
*/
|
|
private $_idx;
|
|
|
|
/**
|
|
* Name of this node, or null if none (e.g., array elements).
|
|
* @var ?string
|
|
*/
|
|
protected $name;
|
|
|
|
/**
|
|
* Parent node of this node, should be set on every node in the
|
|
* document besides the document itself.
|
|
* @var ?Node
|
|
*/
|
|
protected $parent;
|
|
|
|
/**
|
|
* Children nested under this node.
|
|
* @var Node[]
|
|
*/
|
|
protected $children;
|
|
|
|
/**
|
|
* Create a new DOM node.
|
|
*
|
|
* @param ?Node $parent the parent this node is nested under
|
|
* @param ?string $name the name of the node, if one exists
|
|
*/
|
|
public function __construct(?Node $parent, ?string $name)
|
|
{
|
|
$this->parent = $parent;
|
|
$this->children = [];
|
|
$this->name = $name;
|
|
}
|
|
|
|
/**
|
|
* Set the node's name.
|
|
*
|
|
* @param string $name
|
|
* @return void
|
|
*/
|
|
public function setName(string $name): void
|
|
{
|
|
$this->name = $name;
|
|
}
|
|
|
|
/**
|
|
* Get the node's name.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getName(): string
|
|
{
|
|
return $this->name;
|
|
}
|
|
|
|
/**
|
|
* Check whether the node has a name set.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasName(): bool
|
|
{
|
|
return isset($this->name);
|
|
}
|
|
|
|
/**
|
|
* Get this node's parent node.
|
|
*
|
|
* @return Node
|
|
*/
|
|
public function getParent(): Node
|
|
{
|
|
if (!isset($this->parent))
|
|
{
|
|
throw new \Exception('Cannot fetch parent -- none set');
|
|
}
|
|
return $this->parent;
|
|
}
|
|
|
|
/**
|
|
* Set this node's parent.
|
|
*
|
|
* @param Node $parent
|
|
* @return void
|
|
*/
|
|
public function setParent(Node $parent): void
|
|
{
|
|
$this->parent = $parent;
|
|
}
|
|
|
|
/**
|
|
* Check whether this node has any child nodes.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasChildren(): bool
|
|
{
|
|
return sizeof($this->children) > 0;
|
|
}
|
|
|
|
/**
|
|
* How many children this node has.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function countChildren(): int
|
|
{
|
|
return sizeof($this->children);
|
|
}
|
|
|
|
/**
|
|
* Fetch this node's children.
|
|
*
|
|
* @return Node[]
|
|
*/
|
|
public function getChildren(): array
|
|
{
|
|
return $this->children;
|
|
}
|
|
|
|
/**
|
|
* Replace all of this node's children.
|
|
*
|
|
* @param Node[] $nodes
|
|
* @return void
|
|
*/
|
|
public function setChildren(array $nodes): void
|
|
{
|
|
$this->children = $nodes;
|
|
}
|
|
|
|
/**
|
|
* Attempt to find a child node by name.
|
|
*
|
|
* @param string $name
|
|
* @return ?Node
|
|
*/
|
|
public function getChildByName(string $name): ?Node
|
|
{
|
|
foreach ($this->children as $child)
|
|
{
|
|
if ($child->name == $name)
|
|
{
|
|
return $child;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a child of this node by a dot-separated path.
|
|
*
|
|
* Each element of the path can either be a node name or a numeric
|
|
* index representing the offset into an array
|
|
*
|
|
* @param string $path
|
|
* @return ?Node the requested node, or null if it wasn't found
|
|
*/
|
|
public function getChildByPath(string $path): ?Node
|
|
{
|
|
$path = explode('.', $path);
|
|
return $this->findChild($path, $this);
|
|
}
|
|
|
|
/**
|
|
* Recursively walk nodes to find a node based on a path.
|
|
*
|
|
* @param string[] $path
|
|
* @param Node $current
|
|
* @return ?Node
|
|
*/
|
|
protected function findChild(array $path, Node $current): ?Node
|
|
{
|
|
$next = array_shift($path);
|
|
if (is_numeric($next))
|
|
{
|
|
$next = $current[$next];
|
|
}
|
|
else
|
|
{
|
|
$next = $current->getChildByName($next);
|
|
}
|
|
if (empty($path))
|
|
{
|
|
return $next;
|
|
}
|
|
if (!isset($next))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return $this->findChild($path, $next);
|
|
}
|
|
|
|
/**
|
|
* Add a node to this node's children.
|
|
*
|
|
* @param Node $child
|
|
* @return void
|
|
*/
|
|
public function addChild(Node $child): void
|
|
{
|
|
$this->children[] = $child;
|
|
}
|
|
|
|
/**
|
|
* Remove a child node.
|
|
*
|
|
* @param Node $child
|
|
* @return void
|
|
*/
|
|
public function removeChild(Node $child): void
|
|
{
|
|
for ($i = 0; $i < sizeof($this->children); $i++)
|
|
{
|
|
if ($this->children[$i] === $child)
|
|
{
|
|
array_splice($this->children, $i, 1);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Try and determine whether this node is a map (named sub-properties).
|
|
*
|
|
* Checks that this node is not a "FunctionParent" (see isFunctionParent)
|
|
* and whether it contains any children that don't have names.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isMap(): bool
|
|
{
|
|
if ($this->isFunctionParent())
|
|
{
|
|
return false;
|
|
}
|
|
foreach ($this->children as $child)
|
|
{
|
|
if (!$child->hasName())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Try and determine wether this node is an array.
|
|
*
|
|
* Checks that this node is not a "FunctionParent" (see isFunctionParent)
|
|
* and whether it contains any children that have names.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isArray(): bool
|
|
{
|
|
foreach ($this->children as $child)
|
|
{
|
|
if ($child->hasName())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check whether this node is a 'function parent', that is, whether it contains
|
|
* a single child node and that node is a function node.
|
|
*
|
|
* When we come across, in YAML, something like:
|
|
* Map:
|
|
* Value: !Func arg
|
|
* That's parsed as:
|
|
* Map (Node):
|
|
* Value (Node): !Func arg (FunctionNode)
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isFunctionParent(): bool
|
|
{
|
|
return sizeof($this->children) == 1 && $this->children[0] instanceof NodeFunction;
|
|
}
|
|
|
|
/**
|
|
* Remove this node from its parent.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function remove(): void
|
|
{
|
|
$this->getParent()->removeChild($this);
|
|
}
|
|
|
|
// ArrayAccess
|
|
|
|
public function offsetExists($offset): bool
|
|
{
|
|
return isset($this->children[$offset]);
|
|
}
|
|
|
|
public function offsetGet($offset)
|
|
{
|
|
return $this->children[$offset];
|
|
}
|
|
|
|
public function offsetSet($offset, $value)
|
|
{
|
|
$this->children[$offset] = $value;
|
|
}
|
|
|
|
public function offsetUnset($offset)
|
|
{
|
|
unset($this->children[$offset]);
|
|
}
|
|
|
|
// Iterator
|
|
|
|
public function current()
|
|
{
|
|
return $this->children[$this->_idx];
|
|
}
|
|
|
|
public function key()
|
|
{
|
|
return $this->_idx;
|
|
}
|
|
|
|
public function next()
|
|
{
|
|
$this->_idx++;
|
|
}
|
|
|
|
public function rewind()
|
|
{
|
|
$this->_idx = 0;
|
|
}
|
|
|
|
public function valid()
|
|
{
|
|
return isset($this->children[$this->_idx]);
|
|
}
|
|
}
|
|
|