* // some random PHP file * use \SparkLib\Template; * $tpl = new Template('foo.tpl.php'); * $tpl->message = 'Hello world.'; * echo $tpl->render(); * * // in foo.tpl.php * A message: * * * Inside a template, the Template instance may be accessed as $this. * $h() is available as a wrapper around htmlspecialchars(). */ class Template extends HTML implements Renderable { /** * Template filename. */ protected $_template = null; /** * Directory to find template filename in. */ protected $_templateDir; /** * Currently active template - used only during rendering. */ protected $_activeTemplate = null; /** * Array of properties/variables available in template. */ protected $_context = array(); /** * Create a new template. * * Optionally takes a template filename and an associative array of * variables to be passed into the template. * * @param string template filename * @param array optional array of variables to extract() before template is require'd * @param boolean cache template contents? */ public function __construct ($template = null, $context = null) { $this->_templateDir = \LIBDIR . 'templates'; if ($template) $this->setTemplate($template); if (is_array($context)) $this->setContext($context); } /** * Get a new validator for this template's context array. */ public function validator () { return new \SparkLib\Validator($this->_context); } /** * If you want a variable accessible inside the template, set it using * properties of the template object. A reference to the variable will * be extracted as $propertyname. */ public function __set ($property, $value) { return ($this->_context[$property] = $value); } public function __get ($property) { return $this->_context[$property]; } /** * Get the path of the current template directory. */ public function getTemplateDir () { return $this->_templateDir; } /** * Set a path for the template directory. */ public function setTemplateDir ($dir) { return $this->_templateDir = $dir; } /** * Set a path for the template directory, relative to the present one. */ public function setTemplateDirRel ($dir) { return $this->_templateDir = $this->_templateDir . \DIRECTORY_SEPARATOR . $dir; } /** * Set the whole context array at once. Overwrites any existing values. * * If given an instance of Template, will copy that template's entire * context. * * @return $this */ public function setContext ($context = array()) { if (is_array($context)) $this->_context = $context; elseif ($context instanceof Template) $this->_context = $context->getContext(); else throw new \UnexpectedValueException('context must be array or Template instance'); return $this; } /** * Add a set of values to existing context. Overwrites existing values * if the given ones happen to overlap, leaves existing ones in place. * * If given an instance of Template, will copy that template's entire * context. * * @return $this */ public function addContext ($context = array()) { if ($context instanceof Template) $context = $context->getContext(); if (! is_array($context)) throw new \UnexpectedValueException('context must be an array or Template instance'); foreach ($context as $key => $val) { $this->__set($key, $val); } return $this; } /** * Return a copy of the entire context array. */ public function getContext () { return $this->_context; } /** * Set the template file. * * If you want to use a different template directory, see setTemplateDir() * * @param string template file */ public function setTemplate ($template = null) { return $this->_template = $template; } /** * Return filename of current template. */ public function getTemplate () { return $this->_template; } /** * Check if the current template file exists. */ public function templateFileExists ($template = null) { if (is_null($template)) $template = $this->_template; return file_exists($this->getPath($template)); } /** * Render a template. Optionally take a filename to render, * otherwise use the one set in the constructor. */ public function render ($template = null) { // Use $this->_activeTemplate to avoid collisions with variables // extracted into the current scope from $this->_context $this->_activeTemplate = $template ?: $this->_template; try { // Start an output buffer, slurp a template, kill the buffer \ob_start(); $this->slurp($this->_activeTemplate); $output = \ob_get_contents(); \ob_end_clean(); $this->_activeTemplate = null; return $output; } catch (Exception $e) { Fail::log($e); throw $e; } } /** * Wrap render() to return a string if the template object is used in * string context. It is probably safest to avoid relying on this * behavior - an exception thrown here will result in a fatal error. */ public function __toString () { return $this->render(); } /** * Render a template with an alternative variable substitution * syntax, rather than executing it as PHP. * * Unless you're doing some kind of PHP code generation, you * probably don't want or need to use this. */ public function preprocess ($template = null) { // Use $this->_activeTemplate to avoid collisions with variables // extracted into the current scope from $this->_context $this->_activeTemplate = $template ?: $this->_template; try { $output = \file_get_contents($this->_templateDir . \DIRECTORY_SEPARATOR . $this->_activeTemplate); $this->_activeTemplate = null; $pattern = array(); $replace = array(); foreach($this->_context as $key => $val) { if(\is_string($val)) { $pattern[] = '/::' . $key . '::/'; $replace[] = $val; } } $output = \preg_replace($pattern, $replace, $output); return $output; } catch (\Exception $e) { Fail::log($e); throw $e; } } /** * Eval the contents of a template file or a cached copy of same, * with extracted variables in the current scope. * * May be used inside templates to include other templates. * * @param string template filename */ protected function slurp ($template) { // TODO: This can theoretically get overwritten by a 'filename' key // in the context array. There has to be some clever way around this. $filename = $this->getPath($template); // Pull context variables, as references, into this scope. // Try not to change things inside templates - IT WILL BITE YOU. \extract($this->_context, \EXTR_REFS); if (! isset($h)) { $h = function ($text) { return \htmlspecialchars($text); }; } if (! isset($p)) { $p = function ($price) { $class = 'price'; if ($price < 0) $class = 'price neg'; return '' . \htmlspecialchars(\number_format((double)$price, 2, '.', '')) . ''; }; } if (! isset($j)) { $j = function ($var) { return \json_encode(\htmlspecialchars($var)); }; } require $filename; } /** * Get a path for a template. */ protected function getPath ($template) { return $this->_templateDir . \DIRECTORY_SEPARATOR . $template; } /** * Output a PDF file to a given filename. * * @param filespec optional path to output file */ public function outputPDF ($filespec = null) { if (! class_exists('\DOMPDF')) throw new \Exception('DOMPDF appears to be unavailable.'); $pdf = new \DOMPDF(); $pdf->load_html($this->render()); $pdf->render(); if (strlen($filespec) > 0) { \file_put_contents($filespec, $pdf->output()); } else { return $pdf->output(); } } /** * Output a PDF stream to the browser with the given filename. * * @param $name the filename with which the browser will be presented. * @param $options DOMPDF&stream() options. * * This will send the pdf as a document to be opened by default. * Set $options['Attachment'] = 1 if you wish the browser to present * a "Save As" dialog box. * * Other $optionss are: (copied from DOMPDF documentation) * * 'Accept-Ranges' => 1 or 0 - if this is not set to 1, then this * header is not included, off by default this header seems to * have caused some problems despite the fact that it is supposed * to solve them, so I am leaving it off by default. * * 'compress' = > 1 or 0 - apply content stream compression, this is * on (1) by default * */ public function streamPDF ($name = 'output.pdf', $options = null) { if (! class_exists('\DOMPDF')) throw new \Exception('DOMPDF appears to be unavailable.'); $pdf = new \DOMPDF(); $pdf->load_html($this->render()); $pdf->render(); if (\headers_sent()) { throw new \RuntimeException( 'Cannot stream template as PDF since headers have already been sent.' ); } if (! isset($options['Attachment'])) $options['Attachment'] = 0; $pdf->stream($name, $options); } /** * Generate an HTML table row from parameters. * * If the first parameter is a callback, treat it as an anonymous * iterator function which will spit out a class name for the row on * each call. (See iterator(), below, for a quickie way to get one * of these.) * * Will also work with an array. */ protected function tableRow () { $args = func_get_args(); $html = ''; if ((func_num_args() >= 2) && is_callable($args[0])) { // callback for class $striper = array_shift($args); } elseif ((func_num_args() === 1) && is_array($args[0])) { // array instead of multiple params $args = $args[0]; } foreach ($args as &$cell) { $html .= '' . $cell . ''; } return isset($striper) ? '' . $html . '' : '' . $html . ''; } /** * Return an anonymous function which will iterate through its * parameters. Useful for stuff where you want to rotate through, * say, CSS class names or other little blobs of text. Useful with * tableRow(), above. */ protected function iterator () { $list = func_get_args(); $last = count($list) - 1; $i = 0; return function () use (&$i, &$list, &$last) { if ($i > $last) $i = 0; return $list[$i++]; }; } }