cloudformation-plus-plus: cfn template preprocessor
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.
 
 

479 lines
8.5 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);
$current = $this;
while (sizeof($path))
{
$next = array_shift($path);
if (is_numeric($next))
{
$current = $current[$next];
}
else
{
$current = $current->getChildByName($next);
}
if (!isset($current))
{
return null;
}
}
return $current;
}
/**
* Add/replace 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
*
* @todo will explode if you try to write using a numeric index to an array
* @param string $path
* @param Node $node
* @return void
*/
public function setChildByPath(string $path, Node $node): void
{
$path = explode('.', $path);
$current = $this;
while (sizeof($path))
{
$next = array_shift($path);
if (is_numeric($next))
{
$current = $current[$next];
}
else
{
$next_node = $current->getChildByName($next);
if (!isset($next_node))
{
$next_node = new Node($current, $next);
$current->addChild($next_node);
}
$current = $next_node;
}
if (!isset($current))
{
throw new \Exception('Cannot write to non-existent array index by path');
}
}
// Replace current with the passed in node
$current->remove();
$current->getParent()->addChild($node);
$node->setParent($current->getParent());
if ($current->hasName())
{
$node->setName($current->getName());
}
}
/**
* 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);
}
/**
* Convert a node tree containing basic data into native PHP data types.
*
* @param Node $node
* @return array|scalar
*/
public static function toPhp(Node $node)
{
if ($node instanceof NodeValue)
{
return $node->getValue();
}
elseif ($node->isArray())
{
$result = [];
foreach ($node as $child)
{
$result[] = static::toPhp($child);
}
return $result;
}
elseif ($node->isMap())
{
$result = [];
foreach ($node as $child)
{
$result[$child->getName()] = static::toPhp($child);
}
return $result;
}
throw new \Exception('Cannot convert from DOM node');
}
/**
* Convert native PHP data types into a node tree.
*
* @param array|scalar $value
* @return Node
*/
public static function fromPhp($value): Node
{
if (is_scalar($value))
{
return new NodeValue(null, null, $value);
}
elseif (is_array($value))
{
$contains_only_numeric_keys = array_reduce(array_keys($value), static function(bool $carry, string $item): bool {
return $carry && is_numeric($item);
}, true);
if ($contains_only_numeric_keys)
{
$node = new Node(null, null);
foreach ($value as $item)
{
$child = static::fromPhp($item);
$child->setParent($node);
$node->addChild($child);
}
return $node;
}
$node = new Node(null, null);
foreach ($value as $key => $item)
{
$child = static::fromPhp($item);
$child->setName($key);
$child->setParent($node);
$node->addChild($child);
}
return $node;
}
throw new \Exception('Cannot convert passed value into DOM nodes');
}
// 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]);
}
}