Passed
Push — master ( a710a5...961a81 )
by Andreas
02:38
created

BaseEngine::dataMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 2
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);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 111 can also be of type null; however, AirTemplate\BaseEngine::eachGenerator() does only seem to accept array|object, maybe add an additional type check?

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.

Loading history...
127
        }
128
        $rows = 0;
129
        $buffer = '';
130
        foreach ($data as $row) {
0 ignored issues
show
Bug introduced by
The expression $data of type object|array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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