QPTPL.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. <?php
  2. /** @file
  3. * QueryPath templates. See QPTPL.
  4. */
  5. /**
  6. * QPTPL is a template library for QueryPath.
  7. *
  8. * The QPTPL extension provides template tools that can be used in
  9. * conjunction with QueryPath.
  10. *
  11. * There are two basic modes in which this tool operates. Both merge data into
  12. * a pure HTML template. Both base their insertions on classes and IDs in the
  13. * HTML data. Where they differ is in the kind of data merged into the template.
  14. *
  15. * One mode takes array data and does a deep (recursive) merge into the template.
  16. * It can be used for simple substitutions, but it can also be used to loop through
  17. * "rows" of data and create tables.
  18. *
  19. * The second mode takes a classed object and introspects that object to find out
  20. * what CSS classes it is capable of filling. This is one way of bridging an object
  21. * model and QueryPath data.
  22. *
  23. * The unit tests are a good place for documentation, as is the QueryPath webste.
  24. *
  25. * @author M Butcher <matt@aleph-null.tv>
  26. * @license http://opensource.org/licenses/lgpl-2.1.php LGPL or MIT-like license.
  27. * @see QueryPathExtension
  28. * @see QueryPathExtensionRegistry::extend()
  29. * @see https://fedorahosted.org/querypath/wiki/QueryPathTemplate
  30. * @ingroup querypath_extensions
  31. */
  32. class QPTPL implements QueryPathExtension {
  33. protected $qp;
  34. public function __construct(QueryPath $qp) {
  35. $this->qp = $qp;
  36. }
  37. /**
  38. * Apply a template to an object and then insert the results.
  39. *
  40. * This takes a template (an arbitrary fragment of XML/HTML) and an object
  41. * or array and inserts the contents of the object into the template. The
  42. * template is then appended to all of the nodes in the current list.
  43. *
  44. * Note that the data in the object is *not* escaped before it is merged
  45. * into the template. For that reason, an object can return markup (as
  46. * long as it is well-formed).
  47. *
  48. * @param mixed $template
  49. * The template. It can be of any of the types that {@link qp()} supports
  50. * natively. Typically it is a string of XML/HTML.
  51. * @param mixed $object
  52. * Either an object or an associative array.
  53. * - In the case where the parameter
  54. * is an object, this will introspect the object, looking for getters (a la
  55. * Java bean behavior). It will then search the document for CSS classes
  56. * that match the method name. The function is then executed and its contents
  57. * inserted into the document. (If the function returns NULL, nothing is
  58. * inserted.)
  59. * - In the case where the paramter is an associative array, the function will
  60. * look through the template for CSS classes that match the keys of the
  61. * array. When an array key is found, the array value is inserted into the
  62. * DOM as a child of the currently matched element(s).
  63. * @param array $options
  64. * The options for this function. Valid options are:
  65. * - <None defined yet>
  66. * @return QueryPath
  67. * Returns a QueryPath object with all of the changes from the template
  68. * applied into the QueryPath elements.
  69. * @see QueryPath::append()
  70. */
  71. public function tpl($template, $object, $options = array()) {
  72. // Handle default options here.
  73. //$tqp = ($template instanceof QueryPath) ? clone $template: qp($template);
  74. $tqp = qp($template);
  75. if (is_array($object) || $object instanceof Traversable) {
  76. $this->tplArrayR($tqp, $object, $options);
  77. return $this->qp->append($tqp->top());
  78. }
  79. elseif (is_object($object)) {
  80. $this->tplObject($tqp, $object, $options);
  81. }
  82. return $this->qp->append($tqp->top());
  83. }
  84. /**
  85. * Given one template, do substitutions for all objects.
  86. *
  87. * Using this method, one template can be populated from a variety of
  88. * sources. That one template is then appended to the QueryPath object.
  89. * @see tpl()
  90. * @param mixed $template
  91. * The template. It can be of any of the types that {@link qp()} supports
  92. * natively. Typically it is a string of XML/HTML.
  93. * @param array $objects
  94. * An indexed array containing a list of objects or arrays (See {@link tpl()})
  95. * that will be merged into the template.
  96. * @param array $options
  97. * An array of options. See {@link tpl()} for a list.
  98. * @return QueryPath
  99. * Returns the QueryPath object.
  100. */
  101. public function tplAll($template, $objects, $options = array()) {
  102. $tqp = qp($template, ':root');
  103. foreach ($objects as $object) {
  104. if (is_array($object))
  105. $tqp = $this->tplArrayR($tqp, $object, $options);
  106. elseif (is_object($object))
  107. $tqp = $this->tplObject($tqp, $object, $options);
  108. }
  109. return $this->qp->append($tqp->top());
  110. }
  111. /*
  112. protected function tplArray($tqp, $array, $options = array()) {
  113. // If we find something that's not an array, we try to handle it.
  114. if (!is_array($array)) {
  115. is_object($array) ? $this->tplObject($tqp, $array, $options) : $tqp->append($array);
  116. }
  117. // An assoc array means we have mappings of classes to content.
  118. elseif ($this->isAssoc($array)) {
  119. print 'Assoc array found.' . PHP_EOL;
  120. foreach ($array as $key => $value) {
  121. $first = substr($key,0,1);
  122. // We allow classes and IDs if explicit. Otherwise we assume
  123. // a class.
  124. if ($first != '.' && $first != '#') $key = '.' . $key;
  125. if ($tqp->top()->find($key)->size() > 0) {
  126. print "Value: " . $value . PHP_EOL;
  127. if (is_array($value)) {
  128. //$newqp = qp($tqp)->cloneAll();
  129. print $tqp->xml();
  130. $this->tplArray($tqp, $value, $options);
  131. print "Finished recursion\n";
  132. }
  133. else {
  134. print 'QP is ' . $tqp->size() . " inserting value: " . $value . PHP_EOL;
  135. $tqp->append($value);
  136. }
  137. }
  138. }
  139. }
  140. // An indexed array means we have multiple instances of class->content matches.
  141. // We copy the portion of the template and then call repeatedly.
  142. else {
  143. print "Array of arrays found..\n";
  144. foreach ($array as $array2) {
  145. $clone = qp($tqp->xml());
  146. $this->tplArray($clone, $array2, $options);
  147. print "Now appending clone.\n" . $clone->xml();
  148. $tqp->append($clone->parent());
  149. }
  150. }
  151. //return $tqp->top();
  152. return $tqp;
  153. }
  154. */
  155. /**
  156. * Introspect objects to map their functions to CSS classes in a template.
  157. */
  158. protected function tplObject($tqp, $object, $options = array()) {
  159. $ref = new ReflectionObject($object);
  160. $methods = $ref->getMethods();
  161. foreach ($methods as $method) {
  162. if (strpos($method->getName(), 'get') === 0) {
  163. $cssClass = $this->method2class($method->getName());
  164. if ($tqp->top()->find($cssClass)->size() > 0) {
  165. $tqp->append($method->invoke($object));
  166. }
  167. else {
  168. // Revert to the find() that found something.
  169. $tqp->end();
  170. }
  171. }
  172. }
  173. //return $tqp->top();
  174. return $tqp;
  175. }
  176. /**
  177. * Recursively merge array data into a template.
  178. */
  179. public function tplArrayR($qp, $array, $options = NULL) {
  180. // If the value looks primitive, append it.
  181. if (!is_array($array) && !($array instanceof Traversable)) {
  182. $qp->append($array);
  183. }
  184. // If we are dealing with an associative array, traverse it
  185. // and merge as we go.
  186. elseif ($this->isAssoc($array)) {
  187. // Do key/value substitutions
  188. foreach ($array as $k => $v) {
  189. // If no dot or hash, assume class.
  190. $first = substr($k,0,1);
  191. if ($first != '.' && $first != '#') $k = '.' . $k;
  192. // If value is an array, recurse.
  193. if (is_array($v)) {
  194. // XXX: Not totally sure that starting at the
  195. // top is right. Perhaps it should start
  196. // at some other context?
  197. $this->tplArrayR($qp->top($k), $v, $options);
  198. }
  199. // Otherwise, try to append value.
  200. else {
  201. $qp->branch()->children($k)->append($v);
  202. }
  203. }
  204. }
  205. // Otherwise we have an indexed array, and we iterate through
  206. // it.
  207. else {
  208. // Get a copy of the current template and then recurse.
  209. foreach ($array as $entry) {
  210. $eles = $qp->get();
  211. $template = array();
  212. // We manually deep clone the template.
  213. foreach ($eles as $ele) {
  214. $template = $ele->cloneNode(TRUE);
  215. }
  216. $tpl = qp($template);
  217. $tpl = $this->tplArrayR($tpl, $entry, $options);
  218. $qp->before($tpl);
  219. }
  220. // Remove the original template without loosing a handle to the
  221. // newly injected one.
  222. $dead = $qp->branch();
  223. $qp->parent();
  224. $dead->remove();
  225. unset($dead);
  226. }
  227. return $qp;
  228. }
  229. /**
  230. * Check whether an array is associative.
  231. * If the keys of the array are not consecutive integers starting with 0,
  232. * this will return false.
  233. *
  234. * @param array $array
  235. * The array to test.
  236. * @return Boolean
  237. * TRUE if this is an associative array, FALSE otherwise.
  238. */
  239. public function isAssoc($array) {
  240. $i = 0;
  241. foreach ($array as $k => $v) if ($k !== $i++) return TRUE;
  242. // If we get here, all keys passed.
  243. return FALSE;
  244. }
  245. /**
  246. * Convert a function name to a CSS class selector (e.g. myFunc becomes '.myFunc').
  247. * @param string $mname
  248. * Method name.
  249. * @return string
  250. * CSS 3 Class Selector.
  251. */
  252. protected function method2class($mname) {
  253. return '.' . substr($mname, 3);
  254. }
  255. }
  256. QueryPathExtensionRegistry::extend('QPTPL');