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