1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of AirTemplate. |
4
|
|
|
* |
5
|
|
|
* (c) 2016 Andreas Blaser |
6
|
|
|
* |
7
|
|
|
* For the full copyright and license information, please view the LICENSE |
8
|
|
|
* file that was distributed with this source code. |
9
|
|
|
* |
10
|
|
|
* @package AirTemplate |
11
|
|
|
* @author Andreas Blaser <[email protected]> |
12
|
|
|
* @license http://www.spdx.org/licenses/MIT MIT License |
13
|
|
|
*/ |
14
|
|
|
|
15
|
|
|
namespace AirTemplate; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Class BaseEngine |
19
|
|
|
* |
20
|
|
|
* The base render engine class. |
21
|
|
|
* Supports: |
22
|
|
|
* - Multiple field options (Shortcuts/PHP functions, User functions/methods) |
23
|
|
|
*/ |
24
|
|
|
class BaseEngine implements EngineInterface |
25
|
|
|
{ |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Array of parsed templates. |
29
|
|
|
* |
30
|
|
|
* @var array |
31
|
|
|
*/ |
32
|
|
|
protected $templates = []; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Array of fields in templates. |
36
|
|
|
* |
37
|
|
|
* @var array |
38
|
|
|
*/ |
39
|
|
|
protected $fields = []; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Array of field options specified in templates |
43
|
|
|
* |
44
|
|
|
* @var array |
45
|
|
|
*/ |
46
|
|
|
protected $fieldOptions = []; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Translation table for each separator. |
50
|
|
|
* |
51
|
|
|
* @var array |
52
|
|
|
*/ |
53
|
|
|
protected $escapeChars = [ |
54
|
|
|
'\n' => "\n", |
55
|
|
|
'\r' => "\r", |
56
|
|
|
'\t' => "\t", |
57
|
|
|
'\v' => "\v", |
58
|
|
|
'\e' => "\e", |
59
|
|
|
'\f' => "\f", |
60
|
|
|
'\\\\' => "\\", |
61
|
|
|
]; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Constructor. |
65
|
|
|
* |
66
|
|
|
* @param array $templates Array of parsed templates |
67
|
|
|
* @param array $fieldOptions Array of field options |
68
|
|
|
*/ |
69
|
|
|
public function __construct(array $templates, array $fieldOptions) |
70
|
|
|
{ |
71
|
|
|
foreach ($templates as $name => $template) { |
72
|
|
|
$this->templates[$name] = $template['template']; |
73
|
|
|
$this->fields[$name] = $template['fields']; |
74
|
|
|
} |
75
|
|
|
$this->fieldOptions = $fieldOptions; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Renders the template $name using the values in $data. Optionally |
80
|
|
|
* apply specified field rendering options. |
81
|
|
|
* |
82
|
|
|
* @param string $name Template name |
83
|
|
|
* @param array|object $data Replacement values |
84
|
|
|
* |
85
|
|
|
* @return string The rendered output |
86
|
|
|
*/ |
87
|
|
|
public function render($name, $data = []) |
88
|
|
|
{ |
89
|
|
|
if (!isset($this->templates[$name])) { |
90
|
|
|
throw new \RuntimeException('Template "' . $name . '" does not exist.'); |
91
|
|
|
} |
92
|
|
|
return $this->merge($name, $data, is_object($data)); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Repeats the template for each item in $data and return the rendered |
97
|
|
|
* result. Optionally apply specified field rendering options. |
98
|
|
|
* If a function is given in $rowGenerator, each will send each rendered |
99
|
|
|
* row (one by one) to the rowGenerator function. |
100
|
|
|
* There is no return value in this case. |
101
|
|
|
* |
102
|
|
|
* @param string $name Template name |
103
|
|
|
* @param mixed $data Data object or array |
104
|
|
|
* @param string $separator Optional separator between items |
105
|
|
|
* @param \Generator $rowGenerator A row generator function or null |
106
|
|
|
* |
107
|
|
|
* @return string|void The rendered output or nothing in generator mode |
108
|
|
|
*/ |
109
|
|
|
public function each( |
110
|
|
|
$name, |
111
|
|
|
$data = [], |
112
|
|
|
$separator = '', |
113
|
|
|
\Generator $rowGenerator = null |
114
|
|
|
) { |
115
|
|
|
if (!isset($this->templates[$name])) { |
116
|
|
|
throw new \RuntimeException( |
117
|
|
|
'Template "' . $name . '" does not exist.' |
118
|
|
|
); |
119
|
|
|
} |
120
|
|
|
if (is_scalar($data)) { |
121
|
|
|
// may happen in xml files when a repeatable element |
122
|
|
|
// occurs only once |
123
|
|
|
$data = [$data]; |
124
|
|
|
} |
125
|
|
|
if (isset($rowGenerator)) { |
126
|
|
|
return $this->eachGenerator($name, $data, $separator, $rowGenerator); |
|
|
|
|
127
|
|
|
} |
128
|
|
|
$rows = 0; |
129
|
|
|
$buffer = ''; |
130
|
|
|
foreach ($data as $row) { |
|
|
|
|
131
|
|
|
$buffer .= $this->renderRow($name, $row, ($rows > 0 ? $separator : '')); |
132
|
|
|
$rows++; |
133
|
|
|
} |
134
|
|
|
return $buffer; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Repeats the template for each item in $data and return the rendered |
139
|
|
|
* result. Optionally apply specified field rendering options. |
140
|
|
|
* Send each rendered row (one by one) to the rowGenerator function. |
141
|
|
|
* |
142
|
|
|
* @param string $name Template name |
143
|
|
|
* @param array|object $data Data object or array |
144
|
|
|
* @param string $separator Optional separator between items |
145
|
|
|
* @param \Generator $rowGenerator A row generator function |
146
|
|
|
* |
147
|
|
|
* @return void |
148
|
|
|
*/ |
149
|
|
|
public function eachGenerator( |
150
|
|
|
$name, |
151
|
|
|
$data, |
152
|
|
|
$separator, |
153
|
|
|
\Generator $rowGenerator |
154
|
|
|
) { |
155
|
|
|
$rows = 0; |
156
|
|
|
foreach ($data as $row) { |
157
|
|
|
$rowGenerator->send( |
158
|
|
|
$this->renderRow($name, $row, ($rows > 0 ? $separator : '')) |
159
|
|
|
); |
160
|
|
|
$rows++; |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Render a single row. |
166
|
|
|
* |
167
|
|
|
* @param string $name Template name |
168
|
|
|
* @param mixed $row Raw row data |
169
|
|
|
* @param string $separator Optional separator between items |
170
|
|
|
* |
171
|
|
|
* @return string |
172
|
|
|
*/ |
173
|
|
|
private function renderRow($name, $row, $separator) |
174
|
|
|
{ |
175
|
|
|
if (is_scalar($row)) { |
176
|
|
|
$row = ['item' => $row]; |
177
|
|
|
} elseif (is_object($row) && count($row) == 0) { |
178
|
|
|
// seems the only way to identify objs with only one member |
179
|
|
|
$row = ['item' => (string) $row[0]]; |
180
|
|
|
} elseif (is_null($row)) { |
181
|
|
|
return ''; |
182
|
|
|
} |
183
|
|
|
return $separator . $this->merge($name, $row, is_object($row)); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Merge template and values and return the rendered string. Replacement |
188
|
|
|
* values may be specified as an assoc array or an object. |
189
|
|
|
* |
190
|
|
|
* @param string $name Template name |
191
|
|
|
* @param array|object $data Data object or array |
192
|
|
|
* @param bool $isObject Template name |
193
|
|
|
* |
194
|
|
|
* @return string The rendered result |
195
|
|
|
*/ |
196
|
|
|
protected function merge($name, $data, $isObject = false) |
197
|
|
|
{ |
198
|
|
|
$result = ''; |
199
|
|
|
foreach ($this->templates[$name] as $index => $fragment) { |
200
|
|
|
if (!isset($this->fields[$name][$index])) { |
201
|
|
|
$result .= $fragment; |
202
|
|
|
continue; |
203
|
|
|
} |
204
|
|
|
$field = $this->fields[$name][$index]; |
205
|
|
|
$value = self::getFieldValue($field, $data, $isObject); |
206
|
|
View Code Duplication |
if ($this->fieldOptions[$name][$field] !== false) { |
|
|
|
|
207
|
|
|
$value = $this->renderField( |
208
|
|
|
$field, |
209
|
|
|
$value, |
210
|
|
|
$data, |
211
|
|
|
$this->fieldOptions[$name][$field], |
212
|
|
|
$isObject |
213
|
|
|
); |
214
|
|
|
} |
215
|
|
|
$result .= $value; |
216
|
|
|
} |
217
|
|
|
return $result; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Renders the output according to the specified option. |
222
|
|
|
* Options: |
223
|
|
|
* |
224
|
|
|
* @param string $field Field name |
225
|
|
|
* @param mixed $value Replacement value |
226
|
|
|
* @param array|object $data Data array or object |
227
|
|
|
* @param mixed $options Fields options |
228
|
|
|
* @param bool $isObject true if $data is an object |
229
|
|
|
* |
230
|
|
|
* @return string The formatted value |
231
|
|
|
*/ |
232
|
|
|
protected function renderField( |
233
|
|
|
$field, |
234
|
|
|
$value, |
235
|
|
|
$data, |
236
|
|
|
$options, |
237
|
|
|
$isObject |
238
|
|
|
) { |
239
|
|
|
foreach ($options as $option) { |
240
|
|
|
if ($option[0] == 'default:' && empty($value)) { |
241
|
|
|
$value = $option[1]; |
242
|
|
|
break; |
243
|
|
|
} |
244
|
|
|
if ($option[0] == 'php:') { |
245
|
|
|
$value = $this->phpFunction($value, $option[1]); |
246
|
|
|
continue; |
247
|
|
|
} |
248
|
|
|
if ($isObject) { |
249
|
|
|
$option = $this->dataMethod($option, $data); |
250
|
|
|
} |
251
|
|
|
if ($option[0] == 'user:') { |
252
|
|
|
$option = $option[1]; |
253
|
|
|
} |
254
|
|
|
if (is_callable($option)) { |
255
|
|
|
$value = $option($value, $field, $data); |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
return $value; |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Apply a data object method to the value. |
264
|
|
|
* |
265
|
|
|
* @param array $option A single option |
266
|
|
|
* @param array|object $data Data array or object |
267
|
|
|
* |
268
|
|
|
* @return array |
269
|
|
|
*/ |
270
|
|
|
protected function dataMethod(array $option, $data) |
271
|
|
|
{ |
272
|
|
|
if ($option[0] == 'data::') { |
273
|
|
|
$option = [get_class($data), $option[1]]; |
274
|
|
|
} elseif ($option[0] == 'data:') { |
275
|
|
|
$option = [$data, $option[1]]; |
276
|
|
|
} |
277
|
|
|
return $option; |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* Apply a PHP function to the value. |
282
|
|
|
* |
283
|
|
|
* @param string $value Field value |
284
|
|
|
* @param array $option A single option |
285
|
|
|
* |
286
|
|
|
* @return string Field value |
287
|
|
|
*/ |
288
|
|
|
protected function phpFunction($value, $option) |
289
|
|
|
{ |
290
|
|
|
if (is_string($option)) { |
291
|
|
|
if (is_callable($option)) { |
292
|
|
|
return $option($value); |
293
|
|
|
} |
294
|
|
|
return $value; |
295
|
|
|
} |
296
|
|
|
if (is_callable($option[0])) { |
297
|
|
|
return $this->phpUserFunction($value, $option); |
298
|
|
|
} |
299
|
|
|
return $value; |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
/** |
303
|
|
|
* Apply a PHP function to the value. |
304
|
|
|
* |
305
|
|
|
* @param string $value Field value |
306
|
|
|
* @param array $option A single option |
307
|
|
|
* |
308
|
|
|
* @return string Field value |
309
|
|
|
*/ |
310
|
|
|
protected function phpUserFunction($value, $option) |
311
|
|
|
{ |
312
|
|
|
foreach ($option[1] as $k => $v) { |
313
|
|
|
$option[1][$k] = (trim($v) === '?') ? $value : $v; |
314
|
|
|
if (is_numeric($option[1][$k])) { |
315
|
|
|
if (is_float($option[1][$k])) { |
316
|
|
|
$option[1][$k] = floatval($option[1][$k]); |
317
|
|
|
} elseif (is_int($option[1][$k])) { |
318
|
|
|
$option[1][$k] = intval($option[1][$k]); |
319
|
|
|
} |
320
|
|
|
} |
321
|
|
|
} |
322
|
|
|
return call_user_func_array($option[0], $option[1]); |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* get the value for a field or an empty string. |
327
|
|
|
* |
328
|
|
|
* @param string $field Field name |
329
|
|
|
* @param array|object $data Data array or object |
330
|
|
|
* @param bool $isObject true if $data is an object |
331
|
|
|
* |
332
|
|
|
* @return mixed Field value or empty string |
333
|
|
|
*/ |
334
|
|
|
private static function getFieldValue($field, $data, $isObject) |
335
|
|
|
{ |
336
|
|
|
if ($isObject) { |
337
|
|
|
if (isset($data->$field)) { |
338
|
|
|
return $data->$field; |
339
|
|
|
} |
340
|
|
|
return ''; |
341
|
|
|
} |
342
|
|
|
if (isset($data[$field])) { |
343
|
|
|
return $data[$field]; |
344
|
|
|
} |
345
|
|
|
return ''; |
346
|
|
|
} |
347
|
|
|
} |
348
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.