Test Failed
Push — main ( 88b8ff...27a594 )
by Dimitri
03:42
created

Parser::setConditionalDelimiters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 6
ccs 0
cts 3
cp 0
crap 2
rs 10
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, array<string>|callable|string>
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
        $this->plugins = $config['plugins'] ?? [];
69
70
        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
        $start = microtime(true);
82
        if ($saveData === null) {
83
            $saveData = $this->saveData;
84
        }
85
86
        $fileExt = pathinfo($view, PATHINFO_EXTENSION);
87
        $view    = empty($fileExt) ? $view . '.php' : $view; // allow Views as .html, .tpl, etc (from CI3)
88
89
        $cacheName = $options['cache_name'] ?? str_replace('.php', '', $view);
90
91
        // Was it cached?
92
        if (isset($options['cache']) && ($output = cache($cacheName))) {
93
            $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
        $file = $this->viewPath . $view;
99
100
        if (! is_file($file)) {
101
            $fileOrig = $file;
102
            $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
                throw ViewException::invalidFile($fileOrig);
107
            }
108
        }
109
110
        if ($this->tempData === null) {
111
            $this->tempData = $this->data;
112
        }
113
114
        $template = file_get_contents($file);
115
        $output   = $this->parse($template, $this->tempData, $options);
116
        $this->logPerformance($start, microtime(true), $view);
117
118
        if ($saveData) {
119
            $this->data = $this->tempData;
120
        }
121
122
        $output = $this->decorate($output);
123
124
        // Should we cache?
125
        if (isset($options['cache'])) {
126
            cache()->save($cacheName, $output, (int) $options['cache']);
127
        }
128
        $this->tempData = null;
129
130
        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
        $start = microtime(true);
142
        if ($saveData === null) {
143
            $saveData = $this->saveData;
144
        }
145
146
        if ($this->tempData === null) {
147
            $this->tempData = $this->data;
148
        }
149
150
        $output = $this->parse($template, $this->tempData, $options);
151
152
        $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
            $this->data = $this->tempData;
156
        }
157
158
        $this->tempData = null;
159
160
        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 (! empty($context)) {
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
        $this->tempData ??= $this->data;
189
        $this->tempData = array_merge($this->tempData, $data);
190
191
        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
            return '';
206
        }
207
208
        // Remove any possible PHP tags since we don't support it
209
        // and parseConditionals needs it clean anyway...
210
        $template = str_replace(['<?', '?>'], ['&lt;?', '?&gt;'], $template);
211
212
        $template = $this->parseComments($template);
213
        $template = $this->extractNoparse($template);
214
215
        // Replace any conditional code here so we don't have to parse as much
216
        $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
        $template = $this->parsePlugins($template);
221
222
        // loop over the data variables, replacing
223
        // the content as we go.
224
        foreach ($data as $key => $val) {
225
            $escape = true;
226
227
            if (is_array($val)) {
228
                $escape  = false;
229
                $replace = $this->parsePair($key, $val, $template);
230
            } else {
231
                $replace = $this->parseSingle($key, (string) $val);
232
            }
233
234
            foreach ($replace as $pattern => $content) {
235
                $template = $this->replaceSingle($pattern, $content, $template, $escape);
236
            }
237
        }
238
239
        return $this->insertNoparse($template);
240
    }
241
242
    /**
243
     * Parse a single key/value, extracting it
244
     */
245
    protected function parseSingle(string $key, string $val): array
246
    {
247
        $pattern = '#' . $this->leftDelimiter . '!?\s*' . preg_quote($key, '#')
248
            . '(?(?=\s*\|\s*)(\s*\|*\s*([|\w<>=\(\),:.\-\s\+\\\\/]+)*\s*))(\s*)!?'
249
            . $this->rightDelimiter . '#ums';
250
251
        return [$pattern => $val];
252
    }
253
254
    /**
255
     * Parse a tag pair
256
     *
257
     * Parses tag pairs: {some_tag} string... {/some_tag}
258
     */
259
    protected function parsePair(string $variable, array $data, string $template): array
260
    {
261
        // Holds the replacement patterns and contents
262
        // that will be used within a preg_replace in parse()
263
        $replace = [];
264
265
        // Find all matches of space-flexible versions of {tag}{/tag} so we
266
        // have something to loop over.
267
        preg_match_all(
268
            '#' . $this->leftDelimiter . '\s*' . preg_quote($variable, '#') . '\s*' . $this->rightDelimiter . '(.+?)' .
269
            $this->leftDelimiter . '\s*/' . preg_quote($variable, '#') . '\s*' . $this->rightDelimiter . '#us',
270
            $template,
271
            $matches,
272
            PREG_SET_ORDER
273
        );
274
275
        /*
276
         * Each match looks like:
277
         *
278
         * $match[0] {tag}...{/tag}
279
         * $match[1] Contents inside the tag
280
         */
281
        foreach ($matches as $match) {
282
            // Loop over each piece of $data, replacing
283
            // its contents so that we know what to replace in parse()
284
            $str = '';  // holds the new contents for this tag pair.
285
286
            foreach ($data as $row) {
287
                // Objects that have a `toArray()` method should be
288
                // converted with that method (i.e. Entities)
289
                if (is_object($row) && method_exists($row, 'toArray')) {
290
                    $row = $row->toArray();
291
                }
292
                // Otherwise, cast as an array and it will grab public properties.
293
                elseif (is_object($row)) {
294
                    $row = (array) $row;
295
                }
296
297
                $temp  = [];
298
                $pairs = [];
299
                $out   = $match[1];
300
301
                foreach ($row as $key => $val) {
302
                    // For nested data, send us back through this method...
303
                    if (is_array($val)) {
304
                        $pair = $this->parsePair($key, $val, $match[1]);
305
306
                        if (! empty($pair)) {
307
                            $pairs[array_keys($pair)[0]] = true;
308
309
                            $temp = array_merge($temp, $pair);
310
                        }
311
312
                        continue;
313
                    }
314
315
                    if (is_object($val)) {
316
                        $val = 'Class: ' . get_class($val);
317
                    } elseif (is_resource($val)) {
318
                        $val = 'Resource';
319
                    }
320
321
                    $temp['#' . $this->leftDelimiter . '!?\s*' . preg_quote($key, '#') . '(?(?=\s*\|\s*)(\s*\|*\s*([|\w<>=\(\),:.\-\s\+\\\\/]+)*\s*))(\s*)!?' . $this->rightDelimiter . '#us'] = $val;
322
                }
323
324
                // Now replace our placeholders with the new content.
325
                foreach ($temp as $pattern => $content) {
326
                    $out = $this->replaceSingle($pattern, $content, $out, ! isset($pairs[$pattern]));
327
                }
328
329
                $str .= $out;
330
            }
331
332
            $escapedMatch = preg_quote($match[0], '#');
333
334
            $replace['#' . $escapedMatch . '#us'] = $str;
335
        }
336
337
        return $replace;
338
    }
339
340
    /**
341
     * Removes any comments from the file. Comments are wrapped in {# #} symbols:
342
     *
343
     *      {# This is a comment #}
344
     */
345
    protected function parseComments(string $template): string
346
    {
347
        return preg_replace('/\{#.*?#\}/us', '', $template);
348
    }
349
350
    /**
351
     * Extracts noparse blocks, inserting a hash in its place so that
352
     * those blocks of the page are not touched by parsing.
353
     */
354
    protected function extractNoparse(string $template): string
355
    {
356
        $pattern = '/\{\s*noparse\s*\}(.*?)\{\s*\/noparse\s*\}/ums';
357
358
        /*
359
         * $matches[][0] is the raw match
360
         * $matches[][1] is the contents
361
         */
362
        if (preg_match_all($pattern, $template, $matches, PREG_SET_ORDER)) {
363
            foreach ($matches as $match) {
364
                // Create a hash of the contents to insert in its place.
365
                $hash                       = md5($match[1]);
366
                $this->noparseBlocks[$hash] = $match[1];
367
                $template                   = str_replace($match[0], "noparse_{$hash}", $template);
368
            }
369
        }
370
371
        return $template;
372
    }
373
374
    /**
375
     * Re-inserts the noparsed contents back into the template.
376
     */
377
    public function insertNoparse(string $template): string
378
    {
379
        foreach ($this->noparseBlocks as $hash => $replace) {
380
            $template = str_replace("noparse_{$hash}", $replace, $template);
381
            unset($this->noparseBlocks[$hash]);
382
        }
383
384
        return $template;
385
    }
386
387
    /**
388
     * Parses any conditionals in the code, removing blocks that don't
389
     * pass so we don't try to parse it later.
390
     *
391
     * Valid conditionals:
392
     *  - if
393
     *  - elseif
394
     *  - else
395
     */
396
    protected function parseConditionals(string $template): string
397
    {
398
        $leftDelimiter  = preg_quote($this->leftConditionalDelimiter, '/');
399
        $rightDelimiter = preg_quote($this->rightConditionalDelimiter, '/');
400
401
        $pattern = '/'
402
            . $leftDelimiter
403
            . '\s*(if|elseif)\s*((?:\()?(.*?)(?:\))?)\s*'
404
            . $rightDelimiter
405
            . '/ums';
406
407
        /*
408
         * For each match:
409
         * [0] = raw match `{if var}`
410
         * [1] = conditional `if`
411
         * [2] = condition `do === true`
412
         * [3] = same as [2]
413
         */
414
        preg_match_all($pattern, $template, $matches, PREG_SET_ORDER);
415
416
        foreach ($matches as $match) {
417
            // Build the string to replace the `if` statement with.
418
            $condition = $match[2];
419
420
            $statement = $match[1] === 'elseif' ? '<?php elseif (' . $condition . '): ?>' : '<?php if (' . $condition . '): ?>';
421
            $template  = str_replace($match[0], $statement, $template);
422
        }
423
424
        $template = preg_replace(
425
            '/' . $leftDelimiter . '\s*else\s*' . $rightDelimiter . '/ums',
426
            '<?php else: ?>',
427
            $template
428
        );
429
        $template = preg_replace(
430
            '/' . $leftDelimiter . '\s*endif\s*' . $rightDelimiter . '/ums',
431
            '<?php endif; ?>',
432
            $template
433
        );
434
435
        // Parse the PHP itself, or insert an error so they can debug
436
        ob_start();
437
438
        if ($this->tempData === null) {
439
            $this->tempData = $this->data;
440
        }
441
442
        extract($this->tempData);
443
444
        try {
445
            eval('?>' . $template . '<?php ');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
446
        } catch (ParseError $e) {
0 ignored issues
show
Unused Code introduced by
catch (\ParseError $e) 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...
447
            ob_end_clean();
448
449
            throw ViewException::tagSyntaxError(str_replace(['?>', '<?php '], '', $template));
450
        }
451
452
        return ob_get_clean();
453
    }
454
455
    /**
456
     * Over-ride the substitution field delimiters.
457
     *
458
     * @param string $leftDelimiter
459
     * @param string $rightDelimiter
460
     */
461
    public function setDelimiters($leftDelimiter = '{', $rightDelimiter = '}'): static
462
    {
463
        $this->leftDelimiter  = $leftDelimiter;
464
        $this->rightDelimiter = $rightDelimiter;
465
466
        return $this;
467
    }
468
469
    /**
470
     * Over-ride the substitution conditional delimiters.
471
     *
472
     * @param string $leftDelimiter
473
     * @param string $rightDelimiter
474
     */
475
    public function setConditionalDelimiters($leftDelimiter = '{', $rightDelimiter = '}'): static
476
    {
477
        $this->leftConditionalDelimiter  = $leftDelimiter;
478
        $this->rightConditionalDelimiter = $rightDelimiter;
479
480
        return $this;
481
    }
482
483
    /**
484
     * Handles replacing a pseudo-variable with the actual content. Will double-check
485
     * for escaping brackets.
486
     *
487
     * @param array|string $pattern
488
     * @param string       $content
489
     * @param string       $template
490
     */
491
    protected function replaceSingle($pattern, $content, $template, bool $escape = false): string
492
    {
493
        $content = (string) $content;
494
495
        // Replace the content in the template
496
        return preg_replace_callback($pattern, function ($matches) use ($content, $escape) {
497
            // Check for {! !} syntax to not escape this one.
498
            if (
499
                str_starts_with($matches[0], $this->leftDelimiter . '!')
500
                && substr($matches[0], -1 - strlen($this->rightDelimiter)) === '!' . $this->rightDelimiter
501
            ) {
502
                $escape = false;
503
            }
504
505
            return $this->prepareReplacement($matches, $content, $escape);
506
        }, (string) $template);
507
    }
508
509
    /**
510
     * Callback used during parse() to apply any filters to the value.
511
     */
512
    protected function prepareReplacement(array $matches, string $replace, bool $escape = true): string
513
    {
514
        $orig = array_shift($matches);
515
516
        // Our regex earlier will leave all chained values on a single line
517
        // so we need to break them apart so we can apply them all.
518
        $filters = ! empty($matches[1]) ? explode('|', $matches[1]) : [];
519
520
        if ($escape && empty($filters) && ($context = $this->shouldAddEscaping($orig))) {
521
            $filters[] = "esc({$context})";
522
        }
523
524
        return $this->applyFilters($replace, $filters);
525
    }
526
527
    /**
528
     * Checks the placeholder the view provided to see if we need to provide any autoescaping.
529
     *
530
     * @return false|string
531
     */
532
    public function shouldAddEscaping(string $key)
533
    {
534
        $escape = false;
535
536
        $key = trim(str_replace(['{', '}'], '', $key));
537
538
        // If the key has a context stored (from setData)
539
        // we need to respect that.
540
        if (array_key_exists($key, $this->dataContexts)) {
541
            if ($this->dataContexts[$key] !== 'raw') {
542
                return $this->dataContexts[$key];
543
            }
544
        }
545
        // No pipes, then we know we need to escape
546
        elseif (! str_contains($key, '|')) {
547
            $escape = 'html';
548
        }
549
        // If there's a `noescape` then we're definitely false.
550
        elseif (str_contains($key, 'noescape')) {
551
            $escape = false;
552
        }
553
        // If no `esc` filter is found, then we'll need to add one.
554
        elseif (! preg_match('/\s+esc/u', $key)) {
555
            $escape = 'html';
556
        }
557
558
        return $escape;
559
    }
560
561
    /**
562
     * Given a set of filters, will apply each of the filters in turn
563
     * to $replace, and return the modified string.
564
     */
565
    protected function applyFilters(string $replace, array $filters): string
566
    {
567
        // Determine the requested filters
568
        foreach ($filters as $filter) {
569
            // Grab any parameter we might need to send
570
            preg_match('/\([\w<>=\/\\\,:.\-\s\+]+\)/u', $filter, $param);
571
572
            // Remove the () and spaces to we have just the parameter left
573
            $param = ! empty($param) ? trim($param[0], '() ') : null;
574
575
            // Params can be separated by commas to allow multiple parameters for the filter
576
            if (! empty($param)) {
577
                $param = explode(',', $param);
578
579
                // Clean it up
580
                foreach ($param as &$p) {
581
                    $p = trim($p, ' "');
582
                }
583
            } else {
584
                $param = [];
585
            }
586
587
            // Get our filter name
588
            $filter = ! empty($param) ? trim(strtolower(substr($filter, 0, strpos($filter, '(')))) : trim($filter);
589
590
            $this->config['filters'] ??= [];
591
            if (! array_key_exists($filter, $this->config['filters'])) {
592
                continue;
593
            }
594
595
            // Filter it....
596
            $replace = $this->config['filters'][$filter]($replace, ...$param);
597
        }
598
599
        return (string) $replace;
600
    }
601
602
    // Plugins
603
604
    /**
605
     * Scans the template for any parser plugins, and attempts to execute them.
606
     * Plugins are delimited by {+ ... +}
607
     *
608
     * @return string
609
     */
610
    protected function parsePlugins(string $template)
611
    {
612
        foreach ($this->plugins as $plugin => $callable) {
613
            // Paired tags are enclosed in an array in the config array.
614
            $isPair   = is_array($callable);
615
            $callable = $isPair ? array_shift($callable) : $callable;
616
617
            // See https://regex101.com/r/BCBBKB/1
618
            $pattern = $isPair
619
                ? '#\{\+\s*' . $plugin . '([\w=\-_:\+\s\(\)/"@.]*)?\s*\+\}(.+?)\{\+\s*/' . $plugin . '\s*\+\}#uims'
620
                : '#\{\+\s*' . $plugin . '([\w=\-_:\+\s\(\)/"@.]*)?\s*\+\}#uims';
621
622
            /**
623
             * Match tag pairs
624
             *
625
             * Each match is an array:
626
             *   $matches[0] = entire matched string
627
             *   $matches[1] = all parameters string in opening tag
628
             *   $matches[2] = content between the tags to send to the plugin.
629
             */
630
            if (! preg_match_all($pattern, $template, $matches, PREG_SET_ORDER)) {
631
                continue;
632
            }
633
634
            foreach ($matches as $match) {
635
                $params = [];
636
637
                preg_match_all('/([\w-]+=\"[^"]+\")|([\w-]+=[^\"\s=]+)|(\"[^"]+\")|(\S+)/u', trim($match[1]), $matchesParams);
638
639
                foreach ($matchesParams[0] as $item) {
640
                    $keyVal = explode('=', $item);
641
642
                    if (count($keyVal) === 2) {
643
                        $params[$keyVal[0]] = str_replace('"', '', $keyVal[1]);
644
                    } else {
645
                        $params[] = str_replace('"', '', $item);
646
                    }
647
                }
648
649
                $template = $isPair
650
                    ? str_replace($match[0], $callable($match[2], $params), $template)
651
                    : str_replace($match[0], $callable($params), $template);
652
            }
653
        }
654
655
        return $template;
656
    }
657
658
    /**
659
     * Makes a new plugin available during the parsing of the template.
660
     *
661
     * @return $this
662
     */
663
    public function addPlugin(string $alias, callable $callback, bool $isPair = false)
664
    {
665
        $this->plugins[$alias] = $isPair ? [$callback] : $callback;
666
667
        return $this;
668
    }
669
670
    /**
671
     * Removes a plugin from the available plugins.
672
     *
673
     * @return $this
674
     */
675
    public function removePlugin(string $alias)
676
    {
677
        unset($this->plugins[$alias]);
678
679
        return $this;
680
    }
681
682
    /**
683
     * Converts an object to an array, respecting any
684
     * toArray() methods on an object.
685
     *
686
     * @param array|bool|float|int|object|string|null $value
687
     *
688
     * @return array|bool|float|int|string|null
689
     */
690
    protected function objectToArray($value)
691
    {
692
        // Objects that have a `toArray()` method should be
693
        // converted with that method (i.e. Entities)
694
        if (is_object($value) && method_exists($value, 'toArray')) {
695
            $value = $value->toArray();
696
        }
697
        // Otherwise, cast as an array and it will grab public properties.
698
        elseif (is_object($value)) {
699
            $value = (array) $value;
700
        }
701
702
        return $value;
703
    }
704
}
705