A modest collection of PHP libraries used at SparkFun.
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.

436 lines
11 KiB

12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
  1. <?php
  2. namespace SparkLib;
  3. use \SparkLib\Renderable;
  4. use \SparkLib\Fail;
  5. /**
  6. * SparkFun's approach to templating in PHP.
  7. *
  8. * A brief dialog:
  9. *
  10. * B: Maybe we should use a template library of some sort.
  11. *
  12. * C: Didn't we just spend all this time scrapping a bunch of Smarty
  13. * templates?
  14. *
  15. * B: You're right. This is dumb. We should just use PHP.
  16. *
  17. * So we did. This class does almost nothing at all. It's used like
  18. * so:
  19. *
  20. * <code>
  21. * // some random PHP file
  22. * use \SparkLib\Template;
  23. * $tpl = new Template('foo.tpl.php');
  24. * $tpl->message = 'Hello world.';
  25. * echo $tpl->render();
  26. *
  27. * // in foo.tpl.php
  28. * A message: <?= $message ?>
  29. * </code>
  30. *
  31. * Inside a template, the Template instance may be accessed as $this.
  32. * $h() is available as a wrapper around htmlspecialchars().
  33. */
  34. class Template extends HTML implements Renderable {
  35. /**
  36. * Template filename.
  37. */
  38. protected $_template = null;
  39. /**
  40. * Directory to find template filename in.
  41. */
  42. protected $_templateDir;
  43. /**
  44. * Currently active template - used only during rendering.
  45. */
  46. protected $_activeTemplate = null;
  47. /**
  48. * Array of properties/variables available in template.
  49. */
  50. protected $_context = array();
  51. /**
  52. * Create a new template.
  53. *
  54. * Optionally takes a template filename and an associative array of
  55. * variables to be passed into the template.
  56. *
  57. * @param string template filename
  58. * @param array optional array of variables to extract() before template is require'd
  59. * @param boolean cache template contents?
  60. */
  61. public function __construct ($template = null, $context = null)
  62. {
  63. $this->_templateDir = \LIBDIR . 'templates';
  64. if ($template)
  65. $this->setTemplate($template);
  66. if (is_array($context))
  67. $this->setContext($context);
  68. }
  69. /**
  70. * Get a new validator for this template's context array.
  71. */
  72. public function validator ()
  73. {
  74. return new \SparkLib\Validator($this->_context);
  75. }
  76. /**
  77. * If you want a variable accessible inside the template, set it using
  78. * properties of the template object. A reference to the variable will
  79. * be extracted as $propertyname.
  80. */
  81. public function __set ($property, $value)
  82. {
  83. return ($this->_context[$property] = $value);
  84. }
  85. public function __get ($property)
  86. {
  87. return $this->_context[$property];
  88. }
  89. /**
  90. * Get the path of the current template directory.
  91. */
  92. public function getTemplateDir ()
  93. {
  94. return $this->_templateDir;
  95. }
  96. /**
  97. * Set a path for the template directory.
  98. */
  99. public function setTemplateDir ($dir)
  100. {
  101. return $this->_templateDir = $dir;
  102. }
  103. /**
  104. * Set a path for the template directory, relative to the present one.
  105. */
  106. public function setTemplateDirRel ($dir)
  107. {
  108. return $this->_templateDir = $this->_templateDir . \DIRECTORY_SEPARATOR . $dir;
  109. }
  110. /**
  111. * Set the whole context array at once. Overwrites any existing values.
  112. *
  113. * If given an instance of Template, will copy that template's entire
  114. * context.
  115. *
  116. * @return $this
  117. */
  118. public function setContext ($context = array())
  119. {
  120. if (is_array($context))
  121. $this->_context = $context;
  122. elseif ($context instanceof Template)
  123. $this->_context = $context->getContext();
  124. else
  125. throw new \UnexpectedValueException('context must be array or Template instance');
  126. return $this;
  127. }
  128. /**
  129. * Add a set of values to existing context. Overwrites existing values
  130. * if the given ones happen to overlap, leaves existing ones in place.
  131. *
  132. * If given an instance of Template, will copy that template's entire
  133. * context.
  134. *
  135. * @return $this
  136. */
  137. public function addContext ($context = array())
  138. {
  139. if ($context instanceof Template)
  140. $context = $context->getContext();
  141. if (! is_array($context))
  142. throw new \UnexpectedValueException('context must be an array or Template instance');
  143. foreach ($context as $key => $val) {
  144. $this->__set($key, $val);
  145. }
  146. return $this;
  147. }
  148. /**
  149. * Return a copy of the entire context array.
  150. */
  151. public function getContext ()
  152. {
  153. return $this->_context;
  154. }
  155. /**
  156. * Set the template file.
  157. *
  158. * If you want to use a different template directory, see setTemplateDir()
  159. *
  160. * @param string template file
  161. */
  162. public function setTemplate ($template = null)
  163. {
  164. return $this->_template = $template;
  165. }
  166. /**
  167. * Return filename of current template.
  168. */
  169. public function getTemplate ()
  170. {
  171. return $this->_template;
  172. }
  173. /**
  174. * Check if the current template file exists.
  175. */
  176. public function templateFileExists ($template = null)
  177. {
  178. if (is_null($template))
  179. $template = $this->_template;
  180. return file_exists($this->getPath($template));
  181. }
  182. /**
  183. * Render a template. Optionally take a filename to render,
  184. * otherwise use the one set in the constructor.
  185. */
  186. public function render ($template = null)
  187. {
  188. // Use $this->_activeTemplate to avoid collisions with variables
  189. // extracted into the current scope from $this->_context
  190. $this->_activeTemplate = $template ?: $this->_template;
  191. try {
  192. // Start an output buffer, slurp a template, kill the buffer
  193. \ob_start();
  194. $this->slurp($this->_activeTemplate);
  195. $output = \ob_get_contents();
  196. \ob_end_clean();
  197. $this->_activeTemplate = null;
  198. return $output;
  199. } catch (Exception $e) {
  200. Fail::log($e);
  201. throw $e;
  202. }
  203. }
  204. /**
  205. * Wrap render() to return a string if the template object is used in
  206. * string context. It is probably safest to avoid relying on this
  207. * behavior - an exception thrown here will result in a fatal error.
  208. */
  209. public function __toString ()
  210. {
  211. return $this->render();
  212. }
  213. /**
  214. * Render a template with an alternative variable substitution
  215. * syntax, rather than executing it as PHP.
  216. *
  217. * Unless you're doing some kind of PHP code generation, you
  218. * probably don't want or need to use this.
  219. */
  220. public function preprocess ($template = null)
  221. {
  222. // Use $this->_activeTemplate to avoid collisions with variables
  223. // extracted into the current scope from $this->_context
  224. $this->_activeTemplate = $template ?: $this->_template;
  225. try {
  226. $output = \file_get_contents($this->_templateDir . \DIRECTORY_SEPARATOR . $this->_activeTemplate);
  227. $this->_activeTemplate = null;
  228. $pattern = array();
  229. $replace = array();
  230. foreach($this->_context as $key => $val) {
  231. if(\is_string($val)) {
  232. $pattern[] = '/::' . $key . '::/';
  233. $replace[] = $val;
  234. }
  235. }
  236. $output = \preg_replace($pattern, $replace, $output);
  237. return $output;
  238. } catch (\Exception $e) {
  239. Fail::log($e);
  240. throw $e;
  241. }
  242. }
  243. /**
  244. * Eval the contents of a template file or a cached copy of same,
  245. * with extracted variables in the current scope.
  246. *
  247. * May be used inside templates to include other templates.
  248. *
  249. * @param string template filename
  250. */
  251. protected function slurp ($template)
  252. {
  253. // TODO: This can theoretically get overwritten by a 'filename' key
  254. // in the context array. There has to be some clever way around this.
  255. $filename = $this->getPath($template);
  256. // Pull context variables, as references, into this scope.
  257. // Try not to change things inside templates - IT WILL BITE YOU.
  258. \extract($this->_context, \EXTR_REFS);
  259. if (! isset($h)) {
  260. $h = function ($text) { return \htmlspecialchars($text); };
  261. }
  262. if (! isset($p)) {
  263. $p = function ($price) {
  264. $class = 'price';
  265. if ($price < 0)
  266. $class = 'price neg';
  267. return '<span class="' . $class . '">'
  268. . \htmlspecialchars(\number_format((double)$price, 2, '.', '')) . '</span>';
  269. };
  270. }
  271. if (! isset($j)) {
  272. $j = function ($var) { return \json_encode(\htmlspecialchars($var)); };
  273. }
  274. require $filename;
  275. }
  276. /**
  277. * Get a path for a template.
  278. */
  279. protected function getPath ($template)
  280. {
  281. return $this->_templateDir . \DIRECTORY_SEPARATOR . $template;
  282. }
  283. /**
  284. * Output a PDF file to a given filename.
  285. *
  286. * @param filespec optional path to output file
  287. */
  288. public function outputPDF ($filespec = null)
  289. {
  290. if (! class_exists('\DOMPDF'))
  291. throw new \Exception('DOMPDF appears to be unavailable.');
  292. $pdf = new \DOMPDF();
  293. $pdf->load_html($this->render());
  294. $pdf->render();
  295. if (strlen($filespec) > 0) {
  296. \file_put_contents($filespec, $pdf->output());
  297. } else {
  298. return $pdf->output();
  299. }
  300. }
  301. /**
  302. * Output a PDF stream to the browser with the given filename.
  303. *
  304. * @param $name the filename with which the browser will be presented.
  305. * @param $options DOMPDF&stream() options.
  306. *
  307. * This will send the pdf as a document to be opened by default.
  308. * Set $options['Attachment'] = 1 if you wish the browser to present
  309. * a "Save As" dialog box.
  310. *
  311. * Other $optionss are: (copied from DOMPDF documentation)
  312. *
  313. * 'Accept-Ranges' => 1 or 0 - if this is not set to 1, then this
  314. * header is not included, off by default this header seems to
  315. * have caused some problems despite the fact that it is supposed
  316. * to solve them, so I am leaving it off by default.
  317. *
  318. * 'compress' = > 1 or 0 - apply content stream compression, this is
  319. * on (1) by default
  320. *
  321. */
  322. public function streamPDF ($name = 'output.pdf', $options = null)
  323. {
  324. if (! class_exists('\DOMPDF'))
  325. throw new \Exception('DOMPDF appears to be unavailable.');
  326. $pdf = new \DOMPDF();
  327. $pdf->load_html($this->render());
  328. $pdf->render();
  329. if (\headers_sent()) {
  330. throw new \RuntimeException(
  331. 'Cannot stream template as PDF since headers have already been sent.'
  332. );
  333. }
  334. if (! isset($options['Attachment']))
  335. $options['Attachment'] = 0;
  336. $pdf->stream($name, $options);
  337. }
  338. /**
  339. * Generate an HTML table row from parameters.
  340. *
  341. * If the first parameter is a callback, treat it as an anonymous
  342. * iterator function which will spit out a class name for the row on
  343. * each call. (See iterator(), below, for a quickie way to get one
  344. * of these.)
  345. *
  346. * Will also work with an array.
  347. */
  348. protected function tableRow ()
  349. {
  350. $args = func_get_args();
  351. $html = '';
  352. if ((func_num_args() >= 2) && is_callable($args[0])) {
  353. // callback for class
  354. $striper = array_shift($args);
  355. } elseif ((func_num_args() === 1) && is_array($args[0])) {
  356. // array instead of multiple params
  357. $args = $args[0];
  358. }
  359. foreach ($args as &$cell) {
  360. $html .= '<td>' . $cell . '</td>';
  361. }
  362. return isset($striper)
  363. ? '<tr class="' . $striper() . '">' . $html . '</tr>'
  364. : '<tr>' . $html . '</tr>';
  365. }
  366. /**
  367. * Return an anonymous function which will iterate through its
  368. * parameters. Useful for stuff where you want to rotate through,
  369. * say, CSS class names or other little blobs of text. Useful with
  370. * tableRow(), above.
  371. */
  372. protected function iterator ()
  373. {
  374. $list = func_get_args();
  375. $last = count($list) - 1;
  376. $i = 0;
  377. return function () use (&$i, &$list, &$last) {
  378. if ($i > $last)
  379. $i = 0;
  380. return $list[$i++];
  381. };
  382. }
  383. }