<?php
|
|
namespace SparkLib;
|
|
|
|
use \SparkLib\Renderable;
|
|
use \SparkLib\Fail;
|
|
|
|
/**
|
|
* SparkFun's approach to templating in PHP.
|
|
*
|
|
* A brief dialog:
|
|
*
|
|
* B: Maybe we should use a template library of some sort.
|
|
*
|
|
* C: Didn't we just spend all this time scrapping a bunch of Smarty
|
|
* templates?
|
|
*
|
|
* B: You're right. This is dumb. We should just use PHP.
|
|
*
|
|
* So we did. This class does almost nothing at all. It's used like
|
|
* so:
|
|
*
|
|
* <code>
|
|
* // 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: <?= $message ?>
|
|
* </code>
|
|
*
|
|
* 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 '<span class="' . $class . '">'
|
|
. \htmlspecialchars(\number_format((double)$price, 2, '.', '')) . '</span>';
|
|
};
|
|
}
|
|
|
|
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 .= '<td>' . $cell . '</td>';
|
|
}
|
|
|
|
return isset($striper)
|
|
? '<tr class="' . $striper() . '">' . $html . '</tr>'
|
|
: '<tr>' . $html . '</tr>';
|
|
}
|
|
|
|
/**
|
|
* 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++];
|
|
};
|
|
}
|
|
|
|
}
|