Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
147 / 147
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Parser
100.00% covered (success)
100.00%
147 / 147
100.00% covered (success)
100.00%
4 / 4
65
100.00% covered (success)
100.00%
1 / 1
 stringifyReplace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 parseArrays
100.00% covered (success)
100.00%
93 / 93
100.00% covered (success)
100.00%
1 / 1
40
 parseConditionals
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
9
 parseScalars
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
14
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\View\Template\Stream;
16
17/**
18 * View stream template parser class
19 *
20 * @category   Pop
21 * @package    Pop\View
22 * @author     Nick Sagona, III <nick@popphp.org>
23 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
24 * @license    https://www.popphp.org/license     New BSD License
25 * @version    5.0.0
26 */
27class Parser
28{
29
30    /**
31     * Cast a substitution value to string for use as str_replace()'s $replace argument,
32     * leaving arrays as-is since str_replace() accepts them natively
33     *
34     * @param  mixed $value
35     * @return array|string
36     */
37    private static function stringifyReplace(mixed $value): array|string
38    {
39        return is_array($value) ? $value : (string)$value;
40    }
41
42    /**
43     * Parse arrays in the template string
44     *
45     * @param  string $template
46     * @param  array  $data
47     * @param  string $output
48     * @return string
49     */
50    public static function parseArrays(string $template, array $data, string $output): string
51    {
52        foreach ($data as $key => $value) {
53            if (is_array($value) || ($value instanceof \ArrayAccess)) {
54                $start = '[{' . $key . '}]';
55                $end   = '[{/' . $key . '}]';
56                if ((str_contains($template, $start)) && (str_contains($template, $end))) {
57                    $loopCode = substr($template, strpos($template, $start));
58                    $loopCode = substr($loopCode, 0, (strpos($loopCode, $end) + strlen($end)));
59
60                    // The marker-stripped loop body and the boundaries/variable names of any in-loop
61                    // [{if(...)}] block are identical for every row (only the per-row truthiness check
62                    // and substituted value differ), so both are derived once here instead of on every
63                    // iteration of the row loop below.
64                    $strippedLoopCode = str_replace($start, '', $loopCode);
65                    $strippedLoopCode = str_replace($end, '', $strippedLoopCode);
66
67                    $ifBlocks = [];
68                    if (str_contains($strippedLoopCode, '[{if(')) {
69                        $matches = [];
70                        preg_match_all('/\[{if/mi', $strippedLoopCode, $matches, PREG_OFFSET_CAPTURE);
71
72                        if (isset($matches[0][0])) {
73                            foreach ($matches[0] as $match) {
74                                $cond = substr($strippedLoopCode, $match[1]);
75                                $cond = substr($cond, 0, strpos($cond, '[{/if}]') + 7);
76                                $var  = substr($cond, strpos($cond, '(') + 1);
77                                $var  = substr($var, 0, strpos($var, ')'));
78                                // If var is an array
79                                if (str_contains($var, '[')) {
80                                    $index = substr($var, (strpos($var, '[') + 1));
81                                    $index = substr($index, 0, strpos($index, ']'));
82                                    $var   = substr($var, 0, strpos($var, '['));
83                                } else {
84                                    $index = null;
85                                }
86                                if (str_contains($cond, '[{else}]')) {
87                                    $then = substr($cond, (strpos($cond, ')}]') + 3));
88                                    $then = substr($then, 0, strpos($then, '[{else}]'));
89                                    $else = substr($cond, (strpos($cond, '[{else}]') + 8));
90                                    $else = substr($else, 0, strpos($else, '[{/if}]'));
91                                } else {
92                                    $then = substr($cond, (strpos($cond, ')}]') + 3));
93                                    $then = substr($then, 0, strpos($then, '[{/if}]'));
94                                    $else = null;
95                                }
96                                $ifBlocks[] = [
97                                    'cond'  => $cond,
98                                    'var'   => $var,
99                                    'index' => $index,
100                                    'then'  => $then,
101                                    'else'  => $else,
102                                ];
103                            }
104                        }
105                    }
106
107                    $outputLoop = '';
108                    $i     = 0;
109                    $total = count($value);
110                    foreach ($value as $ky => $val) {
111                        $loop = $strippedLoopCode;
112                        foreach ($ifBlocks as $ifBlock) {
113                            $varSet = ($ifBlock['index'] !== null) ?
114                                (!empty($val[$ifBlock['var']][$ifBlock['index']])) :
115                                (!empty($val[$ifBlock['var']]));
116
117                            if ($varSet) {
118                                $code = ($ifBlock['index'] !== null) ?
119                                    str_replace('[{' . $ifBlock['var'] . '[' . $ifBlock['index'] . ']}]', self::stringifyReplace($val[$ifBlock['var']][$ifBlock['index']]), $ifBlock['then']) :
120                                    str_replace('[{' . $ifBlock['var'] . '}]', self::stringifyReplace($val[$ifBlock['var']]), $ifBlock['then']);
121                                $loop = str_replace($ifBlock['cond'], $code, $loop);
122                            } elseif ($ifBlock['else'] !== null) {
123                                $loop = str_replace($ifBlock['cond'], $ifBlock['else'], $loop);
124                            } else {
125                                $loop = str_replace($ifBlock['cond'], '', $loop);
126                            }
127                        }
128
129                        // Handle nested array
130                        if (is_array($val) || ($value instanceof \ArrayAccess) || ($val instanceof \ArrayObject)) {
131                            if (is_numeric($ky)) {
132                                $oLoop = $loop;
133                                foreach ($val as $k => $v) {
134                                    // Check is value is stringable
135                                    if ((is_object($v) && method_exists($v, '__toString')) ||
136                                        (!is_object($v) && !is_array($v))) {
137                                        $oLoop = str_replace('[{' . $k . '}]', (string)$v, $oLoop);
138                                    }
139                                }
140                                if (str_contains($oLoop, '[{i}]')) {
141                                    $oLoop = str_replace('[{i}]', (string)($i + 1), $oLoop);
142                                }
143                                $outputLoop .= $oLoop;
144                            } else {
145                                $s = '[{' . $ky . '}]';
146                                $e = '[{/' . $ky . '}]';
147                                if ((str_contains($loop, $s)) && (str_contains($loop, $e))) {
148                                    $l     = $loop;
149                                    $lCode = substr($l, strpos($l, $s));
150                                    $lCode = substr($lCode, 0, (strpos($lCode, $e) + strlen($e)));
151
152                                    $l     = str_replace($s, '', $lCode);
153                                    $l     = str_replace($e, '', $l);
154                                    $oLoop = '';
155                                    $j     = 1;
156
157                                    foreach ($val as $k => $v) {
158                                        // Check is value is stringable
159                                        if ((is_object($v) && method_exists($v, '__toString')) ||
160                                            (!is_object($v) && !is_array($v))) {
161                                            if (str_contains($l, '[{i}]')) {
162                                                $oLoop .= str_replace(['[{key}]', '[{value}]', '[{i}]'], [$k, $v, (string)$j], $l);
163                                            } else {
164                                                $oLoop .= str_replace(['[{key}]', '[{value}]'], [$k, $v], $l);
165                                            }
166                                            $j++;
167                                        }
168                                    }
169                                    $outputLoop = str_replace($lCode, $oLoop, $loop);
170                                }
171                            }
172                        // Handle scalar
173                        } else {
174                            // Check is value is stringable
175                            if ((is_object($val) && method_exists($val, '__toString')) || !is_object($val)) {
176                                if (str_contains($loop, '[{i}]')) {
177                                    $outputLoop .= str_replace(['[{key}]', '[{value}]', '[{i}]'], [$ky, $val, (string)($i + 1)], $loop);
178                                } else {
179                                    $outputLoop .= str_replace(['[{key}]', '[{value}]'], [$ky, $val], $loop);
180                                }
181                            }
182                        }
183                        $i++;
184                        if ($i < $total) {
185                            $outputLoop .= PHP_EOL;
186                        }
187                    }
188                    $output = str_replace($loopCode, $outputLoop, $output);
189                }
190            }
191        }
192
193        return $output;
194    }
195
196    /**
197     * Parse conditionals in the template string
198     *
199     * @param  string $template
200     * @param  array  $data
201     * @param  string $output
202     * @return string
203     */
204    public static function parseConditionals(string $template, array $data, string $output): string
205    {
206        $matches = [];
207        preg_match_all('/\[{if/mi', $template, $matches, PREG_OFFSET_CAPTURE);
208        if (isset($matches[0][0])) {
209            foreach ($matches[0] as $match) {
210                $cond = substr($template, $match[1]);
211                $cond = substr($cond, 0, strpos($cond, '[{/if}]') + 7);
212                $var  = substr($cond, strpos($cond, '(') + 1);
213                $var  = substr($var, 0, strpos($var, ')'));
214
215                // If var is an array
216                if (str_contains($var, '[')) {
217                    $index  = substr($var, (strpos($var, '[') + 1));
218                    $index  = substr($index, 0, strpos($index, ']'));
219                    $var    = substr($var, 0, strpos($var, '['));
220                    $varSet = (!empty($data[$var][$index]));
221                } else {
222                    $index  = null;
223                    $varSet = (!empty($data[$var]));
224                }
225                if (str_contains($cond, '[{else}]')) {
226                    if ($varSet) {
227                        $code = substr($cond, (strpos($cond, ')}]') + 3));
228                        $code = substr($code, 0, strpos($code, '[{else}]'));
229                        $code = ($index !== null) ?
230                            str_replace('[{' . $var . '[' . $index . ']}]', self::stringifyReplace($data[$var][$index]), $code) :
231                            str_replace('[{' . $var . '}]', self::stringifyReplace($data[$var]), $code);
232                        $output = str_replace($cond, $code, $output);
233                    } else {
234                        $code = substr($cond, (strpos($cond, '[{else}]') + 8));
235                        $code = substr($code, 0, strpos($code, '[{/if}]'));
236                        $output = str_replace($cond, $code, $output);
237                    }
238                } else {
239                    if ($varSet) {
240                        $code = substr($cond, (strpos($cond, ')}]') + 3));
241                        $code = substr($code, 0, strpos($code, '[{/if}]'));
242                        $code = ($index !== null) ?
243                            str_replace('[{' . $var . '[' . $index . ']}]', self::stringifyReplace($data[$var][$index]), $code) :
244                            str_replace('[{' . $var . '}]', self::stringifyReplace($data[$var]), $code);
245                        $output = str_replace($cond, $code, $output);
246                    } else {
247                        $output = str_replace($cond, '', $output);
248                    }
249                }
250            }
251        }
252
253        return $output;
254    }
255
256    /**
257     * Parse scalar values in the template string
258     *
259     * @param  array  $data
260     * @param  string $output
261     * @return string
262     */
263    public static function parseScalars(array $data, string $output): string
264    {
265        foreach ($data as $key => $value) {
266            if (is_array($value) && (str_contains($output, '[{' . $key . '['))) {
267                $matches = [];
268                preg_match_all('/\[{' . $key .'\[/mi', $output, $matches, PREG_OFFSET_CAPTURE);
269
270                if (isset($matches[0]) && isset($matches[0][0])) {
271                    $indices = [];
272                    foreach ($matches[0] as $match) {
273                        $i = substr($output, $match[1] + (strlen($key) + 3));
274                        $i = substr($i, 0, strpos($i, ']'));
275                        $indices[] = $i;
276                    }
277                    foreach ($indices as $i) {
278                        if (isset($value[$i])) {
279                            $output = str_replace('[{' . $key . '[' . $i . ']}]', self::stringifyReplace($value[$i]), $output);
280                        } else {
281                            $output = str_replace('[{' . $key . '[' . $i . ']}]', '', $output);
282                        }
283                    }
284                }
285            } else if (!is_array($value) && !($value instanceof \ArrayAccess)) {
286                // Check is value is stringable
287                if ((is_object($value) && method_exists($value, '__toString')) || !is_object($value)) {
288                    $output = str_replace('[{' . $key . '}]', (string)$value, $output);
289                }
290            }
291        }
292
293        return $output;
294    }
295}