Allow an array of variables to be passed to Template::Render()
[hoplite.git] / views / template.php
1 <?php
2 // Hoplite
3 // Copyright (c) 2011 Blue Static
4 //
5 // This program is free software: you can redistribute it and/or modify it
6 // under the terms of the GNU General Public License as published by the Free
7 // Software Foundation, either version 3 of the License, or any later version.
8 //
9 // This program is distributed in the hope that it will be useful, but WITHOUT
10 // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12 // more details.
13 //
14 // You should have received a copy of the GNU General Public License along with
15 // this program. If not, see <http://www.gnu.org/licenses/>.
16
17 namespace hoplite\views;
18
19 require_once HOPLITE_ROOT . '/base/filter.php';
20
21 /*!
22 A Template is initialized with a text file (typically HTML) and can render it
23 with data from some model. It has a short macro expansion system, equivalent
24 to PHP short open tags, but usable on all installations. Template caching of
25 the parsed state is available.
26 */
27 class Template
28 {
29 /*! @var string The name of the template. */
30 protected $template_name = '';
31
32 /*! @var string The compiled template. */
33 protected $data = NULL;
34
35 /*! @var array Variables to provide to the template. */
36 protected $vars = array();
37
38 /*!
39 Creates a new Template with a given name.
40 @param string
41 */
42 private function __construct($name)
43 {
44 $this->template_name = $name;
45 }
46
47 static public function NewWithData($data)
48 {
49 $template = new Template('');
50 $template->data = $template->_ProcessTemplate($data);
51 return $template;
52 }
53
54 static public function NewWithCompiledData($data)
55 {
56 $template = new Template('');
57 $template->data = $data;
58 return $template;
59 }
60
61 /*! Gets the name of the template. */
62 public function template_name() { return $this->template_name; }
63
64 /*! Gets the parsed data of the template. */
65 public function template() { return $this->data; }
66
67 /*! Overload property accessors to set view variables. */
68 public function __get($key)
69 {
70 return $this->vars[$key];
71 }
72 public function __set($key, $value)
73 {
74 $this->vars[$key] = $value;
75 }
76
77 /*! This includes the template and renders it out. */
78 public function Render($vars = array())
79 {
80 $_template = $this->data;
81 $_vars = array_merge($this->vars, $vars);
82 $render = function () use ($_template, $_vars) {
83 extract($_vars);
84 eval('?>' . $_template . '<' . '?');
85 };
86
87 ob_start();
88
89 $error = error_reporting();
90 error_reporting($error & ~E_NOTICE);
91
92 $render();
93
94 error_reporting($error);
95
96 $data = ob_get_contents();
97 ob_end_clean();
98 return $data;
99 }
100
101 /*! @brief Does any pre-processing on the template.
102 This performs the macro expansion. The language is very simple and is merely
103 shorthand for the PHP tags.
104
105 The most common thing needed in templates is string escaped output from an
106 expression. HTML entities are automatically escaped in this format:
107 <p>Hello, {% $user->name %}!</p>
108
109 To specify the type to format, you use the pipe symbol and then one of the
110 following types: str (default; above), int, float, raw.
111 <p>Hello, {% %user->name | str %}</p>
112 <p>Hello, user #{% $user->user_id | int %}</p>
113
114 To evaluate a non-printing expression, simply add a '!' before the first '%':
115 {!% if (!$user->user_id): %}<p>Hello, Guest!</p>{!% endif %}
116
117 @param string Raw template data
118 @return string Executable PHP
119 */
120 protected function _ProcessTemplate($data)
121 {
122 // The parsed output as compiled PHP.
123 $processed = '';
124
125 // If processing a macro, this contains the contents of the macro while
126 // it is being extracted from the template.
127 $macro = '';
128 $in_macro = FALSE;
129
130 $length = strlen($data);
131 $i = 0; // The current position of the iterator.
132 $looking_for_end = FALSE; // Whehter or not an end tag is expected.
133 $line_number = 1; // The current line number.
134 $i_last_line = 0; // The value of |i| at the previous new line, used for column numbering.
135
136 while ($i < $length) {
137 // See how far the current position is from the end of the string.
138 $delta = $length - $i;
139
140 // When a new line is reached, update the counters.
141 if ($data[$i] == "\n") {
142 ++$line_number;
143 $i_last_line = $i;
144 }
145
146 // Check for simple PHP short-tag expansion.
147 if ($delta >= 3 && substr($data, $i, 3) == '{!%') {
148 // If an expansion has already been opened, then it's an error to nest.
149 if ($looking_for_end) {
150 $column = $i - $i_last_line;
151 throw new TemplateException("Unexpected start of expansion at line $line_number:$column");
152 }
153
154 $looking_for_end = TRUE;
155 $processed .= '<' . '?php';
156 $i += 3;
157 continue;
158 }
159 // Check for either the end tag or the start of a macro expansion.
160 else if ($delta >= 2) {
161 $substr = substr($data, $i, 2);
162 // Check for an end tag.
163 if ($substr == '%}') {
164 // If an end tag was encountered without an open tag, that's an error.
165 if (!$looking_for_end) {
166 $column = $i - $i_last_line;
167 throw new TemplateException("Unexpected end of expansion at line $line_number:$column");
168 }
169
170 // If this is a macro, it's time to process it.
171 if ($in_macro)
172 $processed .= $this->_ProcessMacro($macro);
173
174 $looking_for_end = FALSE;
175 $in_macro = FALSE;
176 $processed .= ' ?>';
177 $i += 2;
178 continue;
179 }
180 // Check for the beginning of a macro.
181 else if ($substr == '{%') {
182 // If an expansion has already been opened, then it's an error to nest.
183 if ($looking_for_end) {
184 $column = $i - $i_last_line;
185 throw new TemplateException("Unexpected start of expansion at line $line_number:$column");
186 }
187
188 $processed .= '<' . '?php echo ';
189 $macro = '';
190 $in_macro = TRUE;
191 $looking_for_end = TRUE;
192 $i += 2;
193 continue;
194 }
195 }
196
197 // All other characters go into a storage bin. If currently in a macro,
198 // save off the data separately for parsing.
199 if ($in_macro)
200 $macro .= $data[$i];
201 else
202 $processed .= $data[$i];
203 ++$i;
204 }
205
206 return $processed;
207 }
208
209 /*!
210 Takes the contents of a macro |{% $some_var | int %}|, which is the part in
211 between the open and close brackets (excluding '%') and transforms it into a
212 PHP statement.
213 */
214 protected function _ProcessMacro($macro)
215 {
216 // The pipe operator specifies how to sanitize the output.
217 $formatter_pos = strrpos($macro, '|');
218
219 // No specifier defaults to escaped string.
220 if ($formatter_pos === FALSE)
221 return 'hoplite\\base\\filter\\String(' . $macro . ')';
222
223 // Otherwise, apply the right filter.
224 $formatter = trim(substr($macro, $formatter_pos + 1));
225 $function = '';
226 switch (strtolower($formatter)) {
227 case 'int': $function = 'Int'; break;
228 case 'float': $function = 'Float'; break;
229 case 'str': $function = 'String'; break;
230 case 'raw': $function = 'RawString'; break;
231 default:
232 throw new TemplateException('Invalid macro formatter "' . $formatter . '"');
233 }
234
235 // Now get the expression and return a PHP statement.
236 $expression = trim(substr($macro, 0, $formatter_pos));
237 return 'hoplite\\base\\filter\\' . $function . '(' . $expression . ')';
238 }
239 }
240
241 class TemplateException extends \Exception {}