| <?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++]; | |
|     }; | |
|   } | |
| 
 | |
| }
 |