1: <?php
2: namespace Tokamak\Dom;
3:
4: use DOMDocument;
5: use DOMNode;
6: use SplQueue;
7: use Closure;
8:
9: /**
10: * Class Node
11: * @package Tokamak\Dom
12: * An abstraction over a DOMNode, providing a simple API for
13: * building document and component templates by implementing
14: * the render method.
15: * @throws \RuntimeException
16: */
17: abstract class Node
18: {
19: /**
20: * @var string Tokamak will look for components in this namespace if they are not
21: * defined elsewhere.
22: */
23: protected static $COMPONENT_NAMESPACE = '\Tokamak\Dom\Components\\';
24:
25: /**
26: * @var DOMDocument The underlying DOMDocument instance.
27: * Normally created by Tokamak\Dom\Document instance.
28: */
29: protected $dom;
30:
31: /**
32: * @var SplQueue<DOMNode> Queue of dom nodes to be appended to the parent node.
33: */
34: protected $domNodes;
35:
36: /**
37: * Append one Node instance as a child of another.
38: * Must be implemented differently for Element and Component,
39: * because the former is a single element that can have children,
40: * whereas the latter can represent a list of multiple
41: * top-level elements and components. Meanwhile, Document
42: * appends children to the top-level DOMDocument instance.
43: * @param Node $node An Element or Component to be appended.
44: * @return Node Returns the child node for method chaining.
45: */
46: abstract public function append(Node $node);
47:
48: /**
49: * Syntactic sugar for constructing and then appending a new Element.
50: * Abstracts away the need to pass the ancestor DOMDocument to the child.
51: * @param string $name The element name ('div', 'a', 'body', etc.)
52: * @param array $attributes Associative array of the element's attributes and their values.
53: * For "class", the value can also be an array of class names.
54: * @param string $content The text content of the element.
55: * @param Closure $callback A callback closure to be executed within the context of the child node.
56: * Allows a callback-chaining style for building the DOM tree.
57: * @return Element Returns the child Element for method chaining.
58: */
59: public function appendElement($name, array $attributes = null, $content = '', Closure $callback = null){
60: $child = $this->append(new Element($this->dom, $name, $attributes, $content));
61: if(isset($callback)){
62: $boundCallback = $callback->bindTo($child, $child);
63: $boundCallback();
64: }
65: return $child;
66: }
67:
68: /**
69: * Syntactic sugar for constructing and then appending a new Component.
70: * Abstracts away the need to pass the ancestor DOMDocument to the child.
71: * If a user-defined Component class of the requested name is not found,
72: * will look in the Tokamak\Dom\Components namespace for a built-in component
73: * class.
74: * @param string $name The fully-qualified class name of a user-defined Component,
75: * or the name of a Component class in the Tokamak\Dom\Components namespace.
76: * @param array $data Array of arbitrary data/state to be passed to the component
77: * @return Component Returns the child Element for method chaining.
78: * @param Closure $callback A callback closure to be executed within the context of the child node.
79: * Allows a callback-chaining style for building the DOM tree.
80: * @throws \RuntimeException Thrown if $name does not refer to a defined Component class.
81: */
82: public function appendComponent($name, array $data = null, Closure $callback = null)
83: {
84: if (is_a($name, 'Tokamak\Dom\Component', true)){
85: // The specified $name is a subclass of Component
86: $component = new $name($this->dom, $data);
87: } else if (is_a(self::$COMPONENT_NAMESPACE . $name, 'Tokamak\Dom\Component', true)){
88: // The requested component name is a built-in Component
89: $qualifiedName = self::$COMPONENT_NAMESPACE . $name;
90: $component = new $qualifiedName($this->dom, $data);
91: } else {
92: throw new \RuntimeException("Component $name not defined.");
93: }
94: $child = $this->append($component);
95:
96: if(isset($callback)){
97: $boundCallback = $callback->bindTo($child, $child);
98: $boundCallback();
99: }
100:
101: return $child;
102: }
103:
104: /**
105: * Inject an DOMDocument element as the ancestor of this node.
106: * @param DOMDocument $dom
107: */
108: public function setDom(DOMDocument $dom)
109: {
110: $this->dom = $dom;
111: }
112:
113: /**
114: * Get the underlying DOMDocument ancestor instance.
115: * @return DOMDocument
116: */
117: public function getDom()
118: {
119: return $this->dom;
120: }
121:
122: /**
123: * Add a DOMNode to the queue of nodes that will
124: * be appended to this node's parent.
125: * @param DOMNode $node
126: */
127: protected function addDomNode(DOMNode $node)
128: {
129: if(!isset($this->domNodes)){
130: $this->domNodes = new SplQueue();
131: }
132: $this->domNodes->enqueue($node);
133: }
134:
135: /**
136: * Remove a DOMNode from the queue of nodes
137: * to be appended to the parent and return it.
138: * @return DOMNode
139: */
140: public function getDomNode()
141: {
142: if($this->hasDomNodes()){
143: $node = $this->domNodes->dequeue();
144: return $node;
145: } else {
146: return null;
147: }
148: }
149:
150: /**
151: * Does this instance have remaining DOMNode instances
152: * to be appended to the parent?
153: * @return bool
154: */
155: public function hasDomNodes()
156: {
157: if(!isset($this->domNodes)){
158: return false;
159: }
160: return !$this->domNodes->isEmpty();
161: }
162:
163: /**
164: * Called by constructor.
165: * Must be implemented to define the DOM structure
166: * of an element or component. Works by adding
167: * nodes to the domNodes queue, either explicitly
168: * or via calls to Node::append.
169: * @return void
170: */
171: abstract protected function render();
172: }