*/ class Cfnpp implements ICompile { /** * Current document state. * @var Document */ protected $document; /** * Functions that can be called by the document. * * Signature: * callable(Node $node, Node $function) * * @var array */ protected $functions; /** * Functions that merge two documents, and will be * called with a node from the current state and * document being compiled. * * Signature: * callable(Node $current_state, Node $new_document, Node $function) * * @var array */ protected $merge_functions; public function __construct() { $this->document = new Document(); $this->functions = []; // Suppress, we'll move these out of here later and clean them up. // @phan-suppress-next-line PhanPluginUnknownClosureReturnType $this->registerMergeFunction('Replace', static function(Node $orig_p, Node $tgt_p, Node $t) { // todo: nodefunctionvalue $repl = new Node(null, $tgt_p->hasName() ? $tgt_p->getName() : null); $repl->setChildren($t->getChildren()); return $repl; }); // @phan-suppress-next-line PhanPluginUnknownClosureReturnType $this->registerFunction('Unset', static function(Node $node, Node $func) { }); } /** * Register a function that can be called by the document. * * @param string $name * @param callable $callback callback(Node $node, Node $function): ?Node * @return void */ public function registerFunction(string $name, callable $callback): void { $this->functions[$name] = $callback; } /** * Register a merge function that can be called by the document and passed * both the current state and the new document. * * @param string $name * @param callable $callback callback(Node $current, Node $new_doc, Node $function): ?Node * @return void */ public function registerMergeFunction(string $name, callable $callback): void { $this->merge_functions[$name] = $callback; } /** * Set the current state that new compilations will be applied to. * * @param Document $document * @return void */ public function setDocument(Document $document): void { $this->document = $document; } /** * Get the document representing the current state. * * @return Document */ public function getDocument(): Document { return $this->document; } /** * Apply a new document to the current state. * * @param Document $document * @param IOptions $options * @return void */ public function compile(Document $document, IOptions $options): void { $this->runMergeFunctions($this->document, $document); $this->merge($this->document, $document); $this->runFunctions($this->document); } /** * Performs a basic recursive merge of two dom trees with target overwriting * original. * * - Maps will be merged * - Arrays will be appended * - Other values will be overwritten * - If there's any mismatch (e.g., array in original + map in target), then * everything's just replaced with whatever's in the target. * * The original DOM tree will represent the final state. The target tree will * be clobbered, as many nodes are simply reparented into the original tree * where appropriate. * * @param Node $original * @param Node $target * @return void */ protected function merge(Node $original, Node $target): void { if ($original->isArray() && $target->isArray()) { foreach ($target as $child) { $original->addChild($child); $child->setParent($original); } } elseif ($original->isMap() && $target->isMap()) { foreach ($target as $child) { $orig_child = $original->getChildByName($child->getName()); // If the key doesn't exist on the source, just copy over and we're done if (!isset($orig_child)) { $original->addChild($child); $child->setParent($original); continue; } // If this is a map or array, we need to descend into it if ($child->isMap() || $child->isArray()) { $this->merge($orig_child, $child); } // Otherwise just replace it (nodefunction, nodevalue, whatever) else { $original->removeChild($orig_child); $original->addChild($child); $child->setParent($original); } } } else { // If it's anything else, overwrite $original->remove(); $original->getParent()->addChild($target); $target->setParent($original); } } /** * Walks through two dom trees simultaneously, adds missing nodes from * target to original, and executes any 'merge functions' it encounters * along the way. * * @param Node $original * @param Node $target * @return void */ protected function runMergeFunctions(Node $original, Node $target): void { if ($target->isFunctionParent() && isset($this->merge_functions[$target[0]->getName()])) { $function_node = $target[0]; $result = $this->runMergeFunction($original, $target, $function_node); $original->remove(); $target->remove(); if (isset($result)) { $original_node = $result; $original_node->setParent($original->getParent()); $original->getParent()->addChild($original_node); $original = $original_node; $target_node = clone $result; $target_node->setParent($target->getParent()); $target->getParent()->addChild($target_node); $target = $target_node; } } foreach ($target as $node) { if ($node->hasName()) { $orig_child = $original->getChildByName($node->getName()); if (!isset($orig_child)) { $orig_child = new Node($original, $node->getName()); $original->addChild($orig_child); } $this->runMergeFunctions($orig_child, $node); } } if (isset($result)) { $target->remove(); } } /** * Walks through the DOM tree and executes any functions it encounters. * * @param Node $node * @return void */ protected function runFunctions(Node $node): void { if ($node->isFunctionParent() && isset($this->functions[$node[0]->getName()])) { $function_node = $node[0]; $result = $this->runFunction($node, $function_node); $node->remove(); if (isset($result)) { $node->getParent()->addChild($result); $result->setParent($node->getParent()); $node = $result; } else { return; } } foreach ($node as $child) { $this->runFunctions($child); } } /** * Runs the function referenced by the NodeFunction node, and returns the * result. * * @param Node $node * @param NodeFunction $function * @return ?Node */ protected function runFunction(Node $node, NodeFunction $function): ?Node { return $this->functions[$function->getName()]($node, $function); } /** * Runs a merge function referenced by the NodeFunction, and returns the * result. * * @param Node $original * @param Node $target * @param NodeFunction $function * @return ?Node */ protected function runMergeFunction(Node $original, Node $target, NodeFunction $function): ?Node { return $this->merge_functions[$function->getName()]($original, $target, $function); } }