Passed
Push — main ( eb4a74...9116b3 )
by Dimitri
08:20 queued 04:05
created

Parser::parseComments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\View;
13
14
use BlitzPHP\Container\Services;
15
use BlitzPHP\Exceptions\ViewException;
16
use BlitzPHP\View\Adapters\NativeAdapter;
17
use ParseError;
18
19
/**
20
 * Class for parsing pseudo-vars
21
 */
22
class Parser extends NativeAdapter
23
{
24
    /**
25
     * Left delimiter character for pseudo vars
26
     */
27
    public string $leftDelimiter = '{';
28
29
    /**
30
     * Right delimiter character for pseudo vars
31
     */
32
    public string $rightDelimiter = '}';
33
34
    /**
35
     * Left delimiter characters for conditionals
36
     */
37
    protected string $leftConditionalDelimiter = '{';
38
39
    /**
40
     * Right delimiter characters for conditionals
41
     */
42
    protected string $rightConditionalDelimiter = '}';
43
44
    /**
45
     * Stores extracted noparse blocks.
46
     */
47
    protected array $noparseBlocks = [];
48
49
    /**
50
     * Stores any plugins registered at run-time.
51
     *
52
     * @var array<string, callable|list<string>|string>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, callable|list<string>|string> at position 6 could not be parsed: Expected '>' at position 6, but found 'list'.
Loading history...
53
     */
54
    protected array $plugins = [];
55
56
    /**
57
     * Stores the context for each data element
58
     * when set by `setData` so the context is respected.
59
     */
60
    protected array $dataContexts = [];
61
62
    /**
63
     * {@inheritDoc}
64
     */
65
    public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG)
66
    {
67
        // Ensure user plugins override core plugins.
68 4
        $this->plugins = $config['plugins'] ?? [];
69
70 4
        parent::__construct($config, $viewPathLocator, $debug);
71
    }
72
73
    /**
74
     * Parse a template
75
     *
76
     * Parses pseudo-variables contained in the specified template view,
77
     * replacing them with any data that has already been set.
78
     */
79
    public function render(string $view, ?array $options = null, ?bool $saveData = null): string
80
    {
81 4
        $start = microtime(true);
82
        if ($saveData === null) {
83 4
            $saveData = $this->saveData;
84
        }
85
86 4
        $fileExt = pathinfo($view, PATHINFO_EXTENSION);
87 4
        $view    = empty($fileExt) ? $view . '.php' : $view; // allow Views as .html, .tpl, etc (from CI3)
88
89 4
        $cacheName = $options['cache_name'] ?? str_replace('.php', '', $view);
90
91
        // Was it cached?
92
        if (isset($options['cache']) && ($output = cache($cacheName))) {
93 4
            $this->logPerformance($start, microtime(true), $view);
0 ignored issues
show
Bug introduced by
It seems like $start can also be of type string; however, parameter $start of BlitzPHP\View\Adapters\A...apter::logPerformance() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

93
            $this->logPerformance(/** @scrutinizer ignore-type */ $start, microtime(true), $view);
Loading history...
Bug introduced by
It seems like microtime(true) can also be of type string; however, parameter $end of BlitzPHP\View\Adapters\A...apter::logPerformance() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

93
            $this->logPerformance($start, /** @scrutinizer ignore-type */ microtime(true), $view);
Loading history...
94
95
            return $output;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $output returns the type BlitzPHP\Cache\Cache|true which is incompatible with the type-hinted return string.
Loading history...
96
        }
97
98 4
        $file = $this->viewPath . $view;
99
100
        if (! is_file($file)) {
101 4
            $fileOrig = $file;
102 4
            $file     = ($this->locator ?: Services::locator())->locateFile($view, 'Views');
103
104
            // locateFile will return an empty string if the file cannot be found.
105
            if (empty($file)) {
106 4
                throw ViewException::invalidFile($fileOrig);
107
            }
108
        }
109
110
        if ($this->tempData === null) {
111
            $this->tempData = $this->data;
112
        }
113
114 4
        $template = file_get_contents($file);
115 4
        $output   = $this->parse($template, $this->tempData, $options);
116 4
        $this->logPerformance($start, microtime(true), $view);
117
118
        if ($saveData) {
119 4
            $this->data = $this->tempData;
120
        }
121
122 4
        $output = $this->decorate($output);
123
124
        // Should we cache?
125
        if (isset($options['cache'])) {
126 4
            cache()->save($cacheName, $output, (int) $options['cache']);
0 ignored issues
show
Bug introduced by
The method save() does not exist on BlitzPHP\Cache\Cache. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

126
            cache()->/** @scrutinizer ignore-call */ save($cacheName, $output, (int) $options['cache']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
127
        }
128 4
        $this->tempData = null;
129
130 4
        return $output;
131
    }
132
133
    /**
134
     * Parse a String
135
     *
136
     * Parses pseudo-variables contained in the specified string,
137
     * replacing them with any data that has already been set.
138
     */
139
    public function renderString(string $template, ?array $options = null, ?bool $saveData = null): string
140
    {
141 2
        $start = microtime(true);
142
        if ($saveData === null) {
143 2
            $saveData = $this->saveData;
144
        }
145
146
        if ($this->tempData === null) {
147
            $this->tempData = $this->data;
148
        }
149
150 2
        $output = $this->parse($template, $this->tempData, $options);
151
152 2
        $this->logPerformance($start, microtime(true), $this->excerpt($template));
0 ignored issues
show
Bug introduced by
It seems like microtime(true) can also be of type string; however, parameter $end of BlitzPHP\View\Adapters\A...apter::logPerformance() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

152
        $this->logPerformance($start, /** @scrutinizer ignore-type */ microtime(true), $this->excerpt($template));
Loading history...
Bug introduced by
It seems like $start can also be of type string; however, parameter $start of BlitzPHP\View\Adapters\A...apter::logPerformance() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

152
        $this->logPerformance(/** @scrutinizer ignore-type */ $start, microtime(true), $this->excerpt($template));
Loading history...
153
154
        if ($saveData) {
155 2
            $this->data = $this->tempData;
156
        }
157
158 2
        $this->tempData = null;
159
160 2
        return $output;
161
    }
162
163
    /**
164
     * Sets several pieces of view data at once.
165
     * In the Parser, we need to store the context here
166
     * so that the variable is correctly handled within the
167
     * parsing itself, and contexts (including raw) are respected.
168
     *
169
     * @param string|null $context The context to escape it for: html, css, js, url, raw
170
     *                             If 'raw', no escaping will happen
171
     */
172
    public function setData(array $data = [], ?string $context = null): static
173
    {
174
        if ($context !== null && $context !== '' && $context !== '0') {
175
            foreach ($data as $key => &$value) {
176
                if (is_array($value)) {
177
                    foreach ($value as &$obj) {
178
                        $obj = $this->objectToArray($obj);
179
                    }
180
                } else {
181
                    $value = $this->objectToArray($value);
182
                }
183
184
                $this->dataContexts[$key] = $context;
185
            }
186
        }
187
188 2
        $this->tempData ??= $this->data;
189 2
        $this->tempData = array_merge($this->tempData, $data);
190
191 2
        return $this;
192
    }
193
194
    /**
195
     * Parse a template
196
     *
197
     * Parses pseudo-variables contained in the specified template,
198
     * replacing them with the data in the second param
199
     *
200
     * @param array $options Future options
201
     */
202
    protected function parse(string $template, array $data = [], ?array $options = null): string
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

202
    protected function parse(string $template, array $data = [], /** @scrutinizer ignore-unused */ ?array $options = null): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
203
    {
204
        if ($template === '') {
205 2
            return '';
206
        }
207
208
        // Remove any possible PHP tags since we don't support it
209
        // and parseConditionals needs it clean anyway...
210 4
        $template = str_replace(['<?', '?>'], ['&lt;?', '?&gt;'], $template);
211
212 4
        $template = $this->parseComments($template);
213 4
        $template = $this->extractNoparse($template);
214
215
        // Replace any conditional code here so we don't have to parse as much
216 4
        $template = $this->parseConditionals($template);
217
218
        // Handle any plugins before normal data, so that
219
        // it can potentially modify any template between its tags.
220 4
        $template = $this->parsePlugins($template);
221
222
        // Parse stack for each parse type (Single and Pairs)
223 4
        $replaceSingleStack = [];
224 4
        $replacePairsStack  = [];
225
226
        // loop over the data variables, saving regex and data
227
        // for later replacement.
228
        foreach ($data as $key => $val) {
229 4
            $escape = true;
230
231
            if (is_array($val)) {
232 2
                $escape              = false;
233
                $replacePairsStack[] = [
234
                    'replace' => $this->parsePair($key, $val, $template),
235
                    'escape'  => $escape,
236 2
                ];
237
            } else {
238
                $replaceSingleStack[] = [
239
                    'replace' => $this->parseSingle($key, (string) $val),
240
                    'escape'  => $escape,
241 4
                ];
242
            }
243
        }
244
245
        // Merge both stacks, pairs first + single stacks
246
        // This allows for nested data with the same key to be replaced properly
247 4
        $replace = array_merge($replacePairsStack, $replaceSingleStack);
248
249
        // Loop over each replace array item which
250
        // holds all the data to be replaced
251
        foreach ($replace as $replaceItem) {
252
            // Loop over the actual data to be replaced
253
            foreach ($replaceItem['replace'] as $pattern => $content) {
254 4
                $template = $this->replaceSingle($pattern, $content, $template, $replaceItem['escape']);
255
            }
256
        }
257
258 4
        return $this->insertNoparse($template);
259
    }
260
261
    /**
262
     * Parse a single key/value, extracting it
263
     */
264
    protected function parseSingle(string $key, string $val): array
265
    {
266
        $pattern = '#' . $this->leftDelimiter . '!?\s*' . preg_quote($key, '#')
267
            . '(?(?=\s*\|\s*)(\s*\|*\s*([|\w<>=\(\),:.\-\s\+\\\\/]+)*\s*))(\s*)!?'
268 4
            . $this->rightDelimiter . '#ums';
269
270 4
        return [$pattern => $val];
271
    }
272
273
    /**
274
     * Parse a tag pair
275
     *
276
     * Parses tag pairs: {some_tag} string... {/some_tag}
277
     */
278
    protected function parsePair(string $variable, array $data, string $template): array
279
    {
280
        // Holds the replacement patterns and contents
281
        // that will be used within a preg_replace in parse()
282 2
        $replace = [];
283
284
        // Find all matches of space-flexible versions of {tag}{/tag} so we
285
        // have something to loop over.
286
        preg_match_all(
287
            '#' . $this->leftDelimiter . '\s*' . preg_quote($variable, '#') . '\s*' . $this->rightDelimiter . '(.+?)' .
288
            $this->leftDelimiter . '\s*/' . preg_quote($variable, '#') . '\s*' . $this->rightDelimiter . '#us',
289
            $template,
290
            $matches,
291
            PREG_SET_ORDER
292 2
        );
293
294
        /*
295
         * Each match looks like:
296
         *
297
         * $match[0] {tag}...{/tag}
298
         * $match[1] Contents inside the tag
299
         */
300
        foreach ($matches as $match) {
301
            // Loop over each piece of $data, replacing
302
            // its contents so that we know what to replace in parse()
303 2
            $str = '';  // holds the new contents for this tag pair.
304
305
            foreach ($data as $row) {
306
                // Objects that have a `toArray()` method should be
307
                // converted with that method (i.e. Entities)
308
                if (is_object($row) && method_exists($row, 'toArray')) {
309 2
                    $row = $row->toArray();
310
                }
311
                // Otherwise, cast as an array and it will grab public properties.
312
                elseif (is_object($row)) {
313 2
                    $row = (array) $row;
314
                }
315
316 2
                $temp  = [];
317 2
                $pairs = [];
318 2
                $out   = $match[1];
319
320
                foreach ($row as $key => $val) {
321
                    // For nested data, send us back through this method...
322
                    if (is_array($val)) {
323 2
                        $pair = $this->parsePair($key, $val, $match[1]);
324
325
                        if ($pair !== []) {
326 2
                            $pairs[array_keys($pair)[0]] = true;
327
328 2
                            $temp = array_merge($temp, $pair);
329
                        }
330
331 2
                        continue;
332
                    }
333
334
                    if (is_object($val)) {
335 2
                        $val = 'Class: ' . $val::class;
336
                    } elseif (is_resource($val)) {
337 2
                        $val = 'Resource';
338
                    }
339
340 2
                    $temp['#' . $this->leftDelimiter . '!?\s*' . preg_quote($key, '#') . '(?(?=\s*\|\s*)(\s*\|*\s*([|\w<>=\(\),:.\-\s\+\\\\/]+)*\s*))(\s*)!?' . $this->rightDelimiter . '#us'] = $val;
341
                }
342
343
                // Now replace our placeholders with the new content.
344
                foreach ($temp as $pattern => $content) {
345 2
                    $out = $this->replaceSingle($pattern, $content, $out, ! isset($pairs[$pattern]));
346
                }
347
348 2
                $str .= $out;
349
            }
350
351 2
            $escapedMatch = preg_quote($match[0], '#');
352
353 2
            $replace['#' . $escapedMatch . '#us'] = $str;
354
        }
355
356 2
        return $replace;
357
    }
358
359
    /**
360
     * Removes any comments from the file. Comments are wrapped in {# #} symbols:
361
     *
362
     *      {# This is a comment #}
363
     */
364
    protected function parseComments(string $template): string
365
    {
366 4
        return preg_replace('/\{#.*?#\}/us', '', $template);
367
    }
368
369
    /**
370
     * Extracts noparse blocks, inserting a hash in its place so that
371
     * those blocks of the page are not touched by parsing.
372
     */
373
    protected function extractNoparse(string $template): string
374
    {
375 4
        $pattern = '/\{\s*noparse\s*\}(.*?)\{\s*\/noparse\s*\}/ums';
376
377
        /*
378
         * $matches[][0] is the raw match
379
         * $matches[][1] is the contents
380
         */
381
        if (preg_match_all($pattern, $template, $matches, PREG_SET_ORDER)) {
382
            foreach ($matches as $match) {
383
                // Create a hash of the contents to insert in its place.
384
                $hash                       = md5($match[1]);
385
                $this->noparseBlocks[$hash] = $match[1];
386
                $template                   = str_replace($match[0], "noparse_{$hash}", $template);
387
            }
388
        }
389
390 4
        return $template;
391
    }
392
393
    /**
394
     * Re-inserts the noparsed contents back into the template.
395
     */
396
    public function insertNoparse(string $template): string
397
    {
398
        foreach ($this->noparseBlocks as $hash => $replace) {
399 4
            $template = str_replace("noparse_{$hash}", $replace, $template);
400
            unset($this->noparseBlocks[$hash]);
401
        }
402
403 4
        return $template;
404
    }
405
406
    /**
407
     * Parses any conditionals in the code, removing blocks that don't
408
     * pass so we don't try to parse it later.
409
     *
410
     * Valid conditionals:
411
     *  - if
412
     *  - elseif
413
     *  - else
414
     */
415
    protected function parseConditionals(string $template): string
416
    {
417 4
        $leftDelimiter  = preg_quote($this->leftConditionalDelimiter, '/');
418 4
        $rightDelimiter = preg_quote($this->rightConditionalDelimiter, '/');
419
420
        $pattern = '/'
421
            . $leftDelimiter
422
            . '\s*(if|elseif)\s*((?:\()?(.*?)(?:\))?)\s*'
423
            . $rightDelimiter
424 4
            . '/ums';
425
426
        /*
427
         * For each match:
428
         * [0] = raw match `{if var}`
429
         * [1] = conditional `if`
430
         * [2] = condition `do === true`
431
         * [3] = same as [2]
432
         */
433 4
        preg_match_all($pattern, $template, $matches, PREG_SET_ORDER);
434
435
        foreach ($matches as $match) {
436
            // Build the string to replace the `if` statement with.
437
            $condition = $match[2];
438
439
            $statement = $match[1] === 'elseif' ? '<?php elseif (' . $condition . '): ?>' : '<?php if (' . $condition . '): ?>';
440
            $template  = str_replace($match[0], $statement, $template);
441
        }
442
443
        $template = preg_replace(
444
            '/' . $leftDelimiter . '\s*else\s*' . $rightDelimiter . '/ums',
445
            '<?php else: ?>',
446
            $template
447 4
        );
448
        $template = preg_replace(
449
            '/' . $leftDelimiter . '\s*endif\s*' . $rightDelimiter . '/ums',
450
            '<?php endif; ?>',
451
            $template
452 4
        );
453
454
        // Parse the PHP itself, or insert an error so they can debug
455 4
        ob_start();
456
457
        if ($this->tempData === null) {
458
            $this->tempData = $this->data;
459
        }
460
461 4
        extract($this->tempData);
462
463
        try {
464 4
            eval('?>' . $template . '<?php ');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
465
        } catch (ParseError) {
0 ignored issues
show
Unused Code introduced by
catch (\ParseError) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
466
            ob_end_clean();
467
468
            throw ViewException::tagSyntaxError(str_replace(['?>', '<?php '], '', $template));
469
        }
470
471 4
        return ob_get_clean();
472
    }
473
474
    /**
475
     * Over-ride the substitution field delimiters.
476
     */
477
    public function setDelimiters(string $leftDelimiter = '{', string $rightDelimiter = '}'): static
478
    {
479 2
        $this->leftDelimiter  = $leftDelimiter;
480 2
        $this->rightDelimiter = $rightDelimiter;
481
482 2
        return $this;
483
    }
484
485
    /**
486
     * Over-ride the substitution conditional delimiters.
487
     */
488
    public function setConditionalDelimiters(string $leftDelimiter = '{', string $rightDelimiter = '}'): static
489
    {
490
        $this->leftConditionalDelimiter  = $leftDelimiter;
491
        $this->rightConditionalDelimiter = $rightDelimiter;
492
493
        return $this;
494
    }
495
496
    /**
497
     * Handles replacing a pseudo-variable with the actual content. Will double-check
498
     * for escaping brackets.
499
     */
500
    protected function replaceSingle(array|string $pattern, string $content, string $template, bool $escape = false): string
501
    {
502
        // Replace the content in the template
503
        return preg_replace_callback($pattern, function ($matches) use ($content, $escape): string {
504
            // Check for {! !} syntax to not escape this one.
505
            if (
506
                str_starts_with($matches[0], $this->leftDelimiter . '!')
507
                && substr($matches[0], -1 - strlen($this->rightDelimiter)) === '!' . $this->rightDelimiter
508
            ) {
509
                $escape = false;
510
            }
511
512 4
            return $this->prepareReplacement($matches, $content, $escape);
513 4
        }, $template);
514
    }
515
516
    /**
517
     * Callback used during parse() to apply any filters to the value.
518
     */
519
    protected function prepareReplacement(array $matches, string $replace, bool $escape = true): string
520
    {
521 4
        $orig = array_shift($matches);
522
523
        // Our regex earlier will leave all chained values on a single line
524
        // so we need to break them apart so we can apply them all.
525 4
        $filters = ! empty($matches[1]) ? explode('|', $matches[1]) : [];
526
527
        if ($escape && $filters === [] && ($context = $this->shouldAddEscaping($orig))) {
528 4
            $filters[] = "esc({$context})";
529
        }
530
531 4
        return $this->applyFilters($replace, $filters);
532
    }
533
534
    /**
535
     * Checks the placeholder the view provided to see if we need to provide any autoescaping.
536
     *
537
     * @return false|string
538
     */
539
    public function shouldAddEscaping(string $key)
540
    {
541 4
        $escape = false;
542
543 4
        $key = trim(str_replace(['{', '}'], '', $key));
544
545
        // If the key has a context stored (from setData)
546
        // we need to respect that.
547
        if (array_key_exists($key, $this->dataContexts)) {
548
            if ($this->dataContexts[$key] !== 'raw') {
549
                return $this->dataContexts[$key];
550
            }
551
        }
552
        // No pipes, then we know we need to escape
553
        elseif (! str_contains($key, '|')) {
554 4
            $escape = 'html';
555
        }
556
        // If there's a `noescape` then we're definitely false.
557
        elseif (str_contains($key, 'noescape')) {
558
            $escape = false;
559
        }
560
        // If no `esc` filter is found, then we'll need to add one.
561
        elseif (! preg_match('/\s+esc/u', $key)) {
562
            $escape = 'html';
563
        }
564
565 4
        return $escape;
566
    }
567
568
    /**
569
     * Given a set of filters, will apply each of the filters in turn
570
     * to $replace, and return the modified string.
571
     */
572
    protected function applyFilters(string $replace, array $filters): string
573
    {
574
        // Determine the requested filters
575
        foreach ($filters as $filter) {
576
            // Grab any parameter we might need to send
577 4
            preg_match('/\([\w<>=\/\\\,:.\-\s\+]+\)/u', $filter, $param);
578
579
            // Remove the () and spaces to we have just the parameter left
580 4
            $param = $param !== [] ? trim($param[0], '() ') : null;
581
582
            // Params can be separated by commas to allow multiple parameters for the filter
583
            if ($param !== null && $param !== '' && $param !== '0') {
584 4
                $param = explode(',', $param);
585
586
                // Clean it up
587
                foreach ($param as &$p) {
588 4
                    $p = trim($p, ' "');
589
                }
590
            } else {
591
                $param = [];
592
            }
593
594
            // Get our filter name
595 4
            $filter = $param !== [] ? trim(strtolower(substr($filter, 0, strpos($filter, '(')))) : trim($filter);
596
597 4
            $this->config['filters'] ??= [];
598
            if (! array_key_exists($filter, $this->config['filters'])) {
599 4
                continue;
600
            }
601
602
            // Filter it....
603
            $replace = $this->config['filters'][$filter]($replace, ...$param);
604
        }
605
606 4
        return (string) $replace;
607
    }
608
609
    // Plugins
610
611
    /**
612
     * Scans the template for any parser plugins, and attempts to execute them.
613
     * Plugins are delimited by {+ ... +}
614
     */
615
    protected function parsePlugins(string $template): string
616
    {
617
        foreach ($this->plugins as $plugin => $callable) {
618
            // Paired tags are enclosed in an array in the config array.
619
            $isPair   = is_array($callable);
620
            $callable = $isPair ? array_shift($callable) : $callable;
621
622
            // See https://regex101.com/r/BCBBKB/1
623
            $pattern = $isPair
624
                ? '#\{\+\s*' . $plugin . '([\w=\-_:\+\s\(\)/"@.]*)?\s*\+\}(.+?)\{\+\s*/' . $plugin . '\s*\+\}#uims'
625
                : '#\{\+\s*' . $plugin . '([\w=\-_:\+\s\(\)/"@.]*)?\s*\+\}#uims';
626
627
            /**
628
             * Match tag pairs
629
             *
630
             * Each match is an array:
631
             *   $matches[0] = entire matched string
632
             *   $matches[1] = all parameters string in opening tag
633
             *   $matches[2] = content between the tags to send to the plugin.
634
             */
635
            if (! preg_match_all($pattern, $template, $matches, PREG_SET_ORDER)) {
636
                continue;
637
            }
638
639
            foreach ($matches as $match) {
640
                $params = [];
641
642
                preg_match_all('/([\w-]+=\"[^"]+\")|([\w-]+=[^\"\s=]+)|(\"[^"]+\")|(\S+)/u', trim($match[1]), $matchesParams);
643
644
                foreach ($matchesParams[0] as $item) {
645
                    $keyVal = explode('=', $item);
646
647
                    if (count($keyVal) === 2) {
648
                        $params[$keyVal[0]] = str_replace('"', '', $keyVal[1]);
649
                    } else {
650
                        $params[] = str_replace('"', '', $item);
651
                    }
652
                }
653
654
                $template = $isPair
655
                    ? str_replace($match[0], $callable($match[2], $params), $template)
656
                    : str_replace($match[0], $callable($params), $template);
657
            }
658
        }
659
660 4
        return $template;
661
    }
662
663
    /**
664
     * Makes a new plugin available during the parsing of the template.
665
     */
666
    public function addPlugin(string $alias, callable $callback, bool $isPair = false): self
667
    {
668
        $this->plugins[$alias] = $isPair ? [$callback] : $callback;
669
670
        return $this;
671
    }
672
673
    /**
674
     * Removes a plugin from the available plugins.
675
     */
676
    public function removePlugin(string $alias): self
677
    {
678
        unset($this->plugins[$alias]);
679
680
        return $this;
681
    }
682
683
    /**
684
     * Converts an object to an array, respecting any
685
     * toArray() methods on an object.
686
     *
687
     * @param array|bool|float|int|object|string|null $value
688
     *
689
     * @return array|bool|float|int|string|null
690
     */
691
    protected function objectToArray($value)
692
    {
693
        // Objects that have a `toArray()` method should be
694
        // converted with that method (i.e. Entities)
695
        if (is_object($value) && method_exists($value, 'toArray')) {
696
            $value = $value->toArray();
697
        }
698
        // Otherwise, cast as an array and it will grab public properties.
699
        elseif (is_object($value)) {
700
            $value = (array) $value;
701
        }
702
703
        return $value;
704
    }
705
}
706