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 Engine |
19
|
|
|
* |
20
|
|
|
* The extended render engine class. |
21
|
|
|
* Supports: |
22
|
|
|
* - Everything from the BaseEngine class |
23
|
|
|
* - Nested templates: {{field|render("sub-template")}}, {{field|each("sub-template")}} |
24
|
|
|
* - Datapath: {{field=rel-path/to/value}}, {{field=/abs-path/to/value}} |
25
|
|
|
*/ |
26
|
|
|
class Engine extends BaseEngine |
27
|
|
|
{ |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Complexity level (0=low, 1=high) |
31
|
|
|
* |
32
|
|
|
* @var int |
33
|
|
|
*/ |
34
|
|
|
protected $complexity = []; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* Array of 'path' arrays pointing to other locations in data. |
38
|
|
|
* |
39
|
|
|
* @var array |
40
|
|
|
*/ |
41
|
|
|
protected $datapath = []; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Constructor. |
45
|
|
|
* |
46
|
|
|
* @param array $templates Array of parsed templates |
47
|
|
|
* @param array $fieldOptions Array of field options |
48
|
|
|
*/ |
49
|
|
|
public function __construct(array $templates, array $fieldOptions) |
50
|
|
|
{ |
51
|
|
|
parent::__construct($templates, $fieldOptions); |
52
|
|
|
foreach ($templates as $name => $template) { |
53
|
|
|
$this->datapath[$name] = $template['datapath']; |
54
|
|
|
$this->complexity[$name] = $template['complexity']; |
55
|
|
|
} |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Merge template and values and return the rendered string. Replacement |
60
|
|
|
* values may be specified as an assoc array or an object. |
61
|
|
|
* |
62
|
|
|
* @param string $name Template name |
63
|
|
|
* @param array|object $data Data object or array |
64
|
|
|
* @param bool $isObject Template name |
65
|
|
|
* |
66
|
|
|
* @return string The rendered result |
67
|
|
|
*/ |
68
|
|
|
protected function merge($name, $data, $isObject = false) |
69
|
|
|
{ |
70
|
|
|
$result = ''; |
71
|
|
|
if ($this->complexity[$name] < 1) { |
72
|
|
|
return parent::merge($name, $data, $isObject); |
73
|
|
|
} |
74
|
|
|
foreach ($this->templates[$name] as $index => $fragment) { |
75
|
|
|
if (!isset($this->fields[$name][$index])) { |
76
|
|
|
$result .= $fragment; |
77
|
|
|
continue; |
78
|
|
|
} |
79
|
|
|
$field = $this->fields[$name][$index]; |
80
|
|
|
$value = self::getFieldValue( |
81
|
|
|
$field, |
82
|
|
|
$data, |
83
|
|
|
$this->datapath[$name][$field], |
84
|
|
|
$isObject |
85
|
|
|
); |
86
|
|
View Code Duplication |
if ($this->fieldOptions[$name][$field] !== false) { |
|
|
|
|
87
|
|
|
$value = $this->renderField( |
88
|
|
|
$field, |
89
|
|
|
$value, |
90
|
|
|
$data, |
91
|
|
|
$this->fieldOptions[$name][$field], |
92
|
|
|
$isObject |
93
|
|
|
); |
94
|
|
|
} |
95
|
|
|
$result .= $value; |
96
|
|
|
} |
97
|
|
|
return $result; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Renders the output according to the specified option. |
102
|
|
|
* Options: |
103
|
|
|
* |
104
|
|
|
* @param string $field Field name |
105
|
|
|
* @param mixed $value Replacement value |
106
|
|
|
* @param array|object $data Data array or object |
107
|
|
|
* @param mixed $options Fields options |
108
|
|
|
* @param bool $isObject true if $data is an object |
109
|
|
|
* |
110
|
|
|
* @return string The formatted value |
111
|
|
|
*/ |
112
|
|
|
protected function renderField( |
113
|
|
|
$field, |
114
|
|
|
$value, |
115
|
|
|
$data, |
116
|
|
|
$options, |
117
|
|
|
$isObject |
118
|
|
|
) { |
119
|
|
|
foreach ($options as $option) { |
120
|
|
|
if ($option[0] == 'default:' && empty($value)) { |
121
|
|
|
$value = $option[1]; |
122
|
|
|
break; |
123
|
|
|
} |
124
|
|
|
if ($option[0] == 'php:') { |
125
|
|
|
$value = $this->phpFunction($value, $option[1]); |
126
|
|
|
continue; |
127
|
|
|
} |
128
|
|
|
if ($option[0] == 'self:') { |
129
|
|
|
$value = $this->renderSubTemplate($value, $data, $option[1]); |
130
|
|
|
continue; |
131
|
|
|
} |
132
|
|
|
if ($isObject) { |
133
|
|
View Code Duplication |
if ($option[0] == 'data::') { |
|
|
|
|
134
|
|
|
$option = [get_class($data), $option[1]]; |
135
|
|
|
} elseif ($option[0] == 'data:') { |
136
|
|
|
$option = [$data, $option[1]]; |
137
|
|
|
} |
138
|
|
|
$option = $this->dataMethod($option, $data); |
139
|
|
|
} |
140
|
|
|
if ($option[0] == 'user:') { |
141
|
|
|
$option = $option[1]; |
142
|
|
|
} |
143
|
|
|
if (is_callable($option)) { |
144
|
|
|
$value = $option($value, $field, $data); |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
return $value; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Renders a sub-template. |
152
|
|
|
* |
153
|
|
|
* When the ? parameter is NOT set, we go a level deeper, |
154
|
|
|
* otherwise we stay on the same level and we pass $data |
155
|
|
|
* instead of $value to render or each. |
156
|
|
|
* |
157
|
|
|
* @param string $value Field value |
158
|
|
|
* @param array|object $data Data array or object |
159
|
|
|
* @param array $option A single option |
160
|
|
|
* |
161
|
|
|
* @return string Field value |
162
|
|
|
*/ |
163
|
|
|
protected function renderSubTemplate($value, $data, $option) |
164
|
|
|
{ |
165
|
|
|
if ($option[0] == 'each') { |
166
|
|
|
$sep = (isset($option[1][1]) ? $option[1][1] : ''); |
167
|
|
|
if ($sep != '') { |
168
|
|
|
$sep = str_replace( |
169
|
|
|
array_keys($this->escapeChars), |
170
|
|
|
array_values($this->escapeChars), |
171
|
|
|
$sep |
172
|
|
|
); |
173
|
|
|
} |
174
|
|
|
return $this->each($option[1][0], $value, $sep); |
175
|
|
|
} |
176
|
|
|
$value = ( |
177
|
|
|
(isset($option[1][1]) && trim($option[1][1]) == '?') |
178
|
|
|
? $data |
179
|
|
|
: $value |
180
|
|
|
); |
181
|
|
|
return $this->render($option[1][0], $value); |
|
|
|
|
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* get the value for a field or an empty string. |
186
|
|
|
* |
187
|
|
|
* @param string $field Field name |
188
|
|
|
* @param array|object $data Data array or object |
189
|
|
|
* @param array|bool A datapath array or false |
190
|
|
|
* @param bool $isObject true if $data is an object |
191
|
|
|
* |
192
|
|
|
* @return mixed Field value or empty string |
193
|
|
|
*/ |
194
|
|
|
private static function getFieldValue($field, $data, $datapath, $isObject) |
|
|
|
|
195
|
|
|
{ |
196
|
|
|
if ($isObject) { |
197
|
|
|
if ($datapath === false && isset($data->$field)) { |
198
|
|
|
return $data->$field; |
199
|
|
|
} |
200
|
|
|
if ($datapath !== false) { |
201
|
|
|
return self::queryObject($datapath, $data); |
|
|
|
|
202
|
|
|
} |
203
|
|
|
return ''; |
204
|
|
|
} |
205
|
|
|
if ($datapath === false && isset($data[$field])) { |
206
|
|
|
return $data[$field]; |
207
|
|
|
} |
208
|
|
|
if ($datapath !== false) { |
209
|
|
|
return self::queryArray($datapath, $data); |
|
|
|
|
210
|
|
|
} |
211
|
|
|
return ''; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* Lookup a value in the data array. |
216
|
|
|
* |
217
|
|
|
* @param array $keys An array of access keys |
218
|
|
|
* @param array $data Data array |
219
|
|
|
* |
220
|
|
|
* @return mixed Field value or empty string |
221
|
|
|
*/ |
222
|
|
|
private static function queryArray(array $keys, array $data) |
223
|
|
|
{ |
224
|
|
|
$result = $data; |
225
|
|
|
foreach ($keys as $key) { |
226
|
|
|
if (!isset($result[$key])) { |
227
|
|
|
return ''; |
228
|
|
|
} |
229
|
|
|
$result = &$result[$key]; |
230
|
|
|
} |
231
|
|
|
return $result; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Lookup a value in the data object. |
236
|
|
|
* |
237
|
|
|
* @param array $keys An array of access keys |
238
|
|
|
* @param object $data Data object |
239
|
|
|
* |
240
|
|
|
* @return mixed Field value or empty string |
241
|
|
|
*/ |
242
|
|
|
private static function queryObject(array $keys, $data) |
243
|
|
|
{ |
244
|
|
|
$result_a = $data; |
245
|
|
|
$result_o = $data; |
246
|
|
|
foreach ($keys as $key) { |
247
|
|
|
if ($key[0] == '@') { |
248
|
|
|
$key = substr($key, 1); |
249
|
|
|
if (isset($key) && is_a($result_o, 'SimpleXMLElement')) { |
250
|
|
|
return self::queryObjectAttr($key, $result_o); |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
$result_a = (array) $result_a; |
254
|
|
|
if (!isset($result_o->$key) && !isset($result_a[$key])) { |
255
|
|
|
return ''; |
256
|
|
|
} |
257
|
|
|
$result_a = &$result_a[$key]; |
258
|
|
|
$result_o = isset($result_o->$key) ? $result_o->$key : null; |
259
|
|
|
} |
260
|
|
|
return !is_null($result_a) |
261
|
|
|
? $result_a |
262
|
|
|
: (!is_null($result_o) ? $result_o : ''); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Lookup an attribute value in the data element. |
267
|
|
|
* |
268
|
|
|
* @param string $key Property name |
269
|
|
|
* @param object $element Object property |
270
|
|
|
* |
271
|
|
|
* @return mixed Attribute value or empty string |
272
|
|
|
*/ |
273
|
|
|
private static function queryObjectAttr($key, $element) |
274
|
|
|
{ |
275
|
|
|
$attr = $element->attributes(); |
276
|
|
|
if (count($attr) > 0 && isset($attr[$key])) { |
277
|
|
|
return (string) $attr[$key]; |
278
|
|
|
} |
279
|
|
|
return ''; |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.