*/ 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]); } }