1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Twig\Extension;
13
14use Twig\DeprecatedCallableInfo;
15use Twig\Environment;
16use Twig\Error\LoaderError;
17use Twig\Error\RuntimeError;
18use Twig\Error\SyntaxError;
19use Twig\ExpressionParser\Infix\ArrowExpressionParser;
20use Twig\ExpressionParser\Infix\AssignmentExpressionParser;
21use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser;
22use Twig\ExpressionParser\Infix\ConditionalTernaryExpressionParser;
23use Twig\ExpressionParser\Infix\DotExpressionParser;
24use Twig\ExpressionParser\Infix\FilterExpressionParser;
25use Twig\ExpressionParser\Infix\FunctionExpressionParser;
26use Twig\ExpressionParser\Infix\IsExpressionParser;
27use Twig\ExpressionParser\Infix\IsNotExpressionParser;
28use Twig\ExpressionParser\Infix\SquareBracketExpressionParser;
29use Twig\ExpressionParser\InfixAssociativity;
30use Twig\ExpressionParser\PrecedenceChange;
31use Twig\ExpressionParser\Prefix\GroupingExpressionParser;
32use Twig\ExpressionParser\Prefix\LiteralExpressionParser;
33use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser;
34use Twig\Markup;
35use Twig\Node\Expression\AbstractExpression;
36use Twig\Node\Expression\Binary\AddBinary;
37use Twig\Node\Expression\Binary\AndBinary;
38use Twig\Node\Expression\Binary\BitwiseAndBinary;
39use Twig\Node\Expression\Binary\BitwiseOrBinary;
40use Twig\Node\Expression\Binary\BitwiseXorBinary;
41use Twig\Node\Expression\Binary\ConcatBinary;
42use Twig\Node\Expression\Binary\DivBinary;
43use Twig\Node\Expression\Binary\ElvisBinary;
44use Twig\Node\Expression\Binary\EndsWithBinary;
45use Twig\Node\Expression\Binary\EqualBinary;
46use Twig\Node\Expression\Binary\FloorDivBinary;
47use Twig\Node\Expression\Binary\GreaterBinary;
48use Twig\Node\Expression\Binary\GreaterEqualBinary;
49use Twig\Node\Expression\Binary\HasEveryBinary;
50use Twig\Node\Expression\Binary\HasSomeBinary;
51use Twig\Node\Expression\Binary\InBinary;
52use Twig\Node\Expression\Binary\LessBinary;
53use Twig\Node\Expression\Binary\LessEqualBinary;
54use Twig\Node\Expression\Binary\MatchesBinary;
55use Twig\Node\Expression\Binary\ModBinary;
56use Twig\Node\Expression\Binary\MulBinary;
57use Twig\Node\Expression\Binary\NotEqualBinary;
58use Twig\Node\Expression\Binary\NotInBinary;
59use Twig\Node\Expression\Binary\NotSameAsBinary;
60use Twig\Node\Expression\Binary\NullCoalesceBinary;
61use Twig\Node\Expression\Binary\OrBinary;
62use Twig\Node\Expression\Binary\PowerBinary;
63use Twig\Node\Expression\Binary\RangeBinary;
64use Twig\Node\Expression\Binary\SameAsBinary;
65use Twig\Node\Expression\Binary\SpaceshipBinary;
66use Twig\Node\Expression\Binary\StartsWithBinary;
67use Twig\Node\Expression\Binary\SubBinary;
68use Twig\Node\Expression\Binary\XorBinary;
69use Twig\Node\Expression\BlockReferenceExpression;
70use Twig\Node\Expression\Filter\DefaultFilter;
71use Twig\Node\Expression\FunctionNode\EnumCasesFunction;
72use Twig\Node\Expression\FunctionNode\EnumFunction;
73use Twig\Node\Expression\GetAttrExpression;
74use Twig\Node\Expression\ParentExpression;
75use Twig\Node\Expression\Test\ConstantTest;
76use Twig\Node\Expression\Test\DefinedTest;
77use Twig\Node\Expression\Test\DivisiblebyTest;
78use Twig\Node\Expression\Test\EvenTest;
79use Twig\Node\Expression\Test\NullTest;
80use Twig\Node\Expression\Test\OddTest;
81use Twig\Node\Expression\Test\SameasTest;
82use Twig\Node\Expression\Test\TrueTest;
83use Twig\Node\Expression\Unary\NegUnary;
84use Twig\Node\Expression\Unary\NotUnary;
85use Twig\Node\Expression\Unary\PosUnary;
86use Twig\Node\Expression\Unary\SpreadUnary;
87use Twig\Node\Node;
88use Twig\Parser;
89use Twig\Sandbox\SecurityNotAllowedMethodError;
90use Twig\Sandbox\SecurityNotAllowedPropertyError;
91use Twig\Source;
92use Twig\Template;
93use Twig\TemplateWrapper;
94use Twig\TokenParser\ApplyTokenParser;
95use Twig\TokenParser\BlockTokenParser;
96use Twig\TokenParser\DeprecatedTokenParser;
97use Twig\TokenParser\DoTokenParser;
98use Twig\TokenParser\EmbedTokenParser;
99use Twig\TokenParser\ExtendsTokenParser;
100use Twig\TokenParser\FlushTokenParser;
101use Twig\TokenParser\ForTokenParser;
102use Twig\TokenParser\FromTokenParser;
103use Twig\TokenParser\GuardTokenParser;
104use Twig\TokenParser\IfTokenParser;
105use Twig\TokenParser\ImportTokenParser;
106use Twig\TokenParser\IncludeTokenParser;
107use Twig\TokenParser\MacroTokenParser;
108use Twig\TokenParser\SetTokenParser;
109use Twig\TokenParser\TypesTokenParser;
110use Twig\TokenParser\UseTokenParser;
111use Twig\TokenParser\WithTokenParser;
112use Twig\TwigFilter;
113use Twig\TwigFunction;
114use Twig\TwigTest;
115use Twig\Util\CallableArgumentsExtractor;
116
117final class CoreExtension extends AbstractExtension
118{
119    public const ARRAY_LIKE_CLASSES = [
120        'ArrayIterator',
121        'ArrayObject',
122        'CachingIterator',
123        'RecursiveArrayIterator',
124        'RecursiveCachingIterator',
125        'SplDoublyLinkedList',
126        'SplFixedArray',
127        'SplObjectStorage',
128        'SplQueue',
129        'SplStack',
130        'WeakMap',
131    ];
132
133    private const DEFAULT_TRIM_CHARS = " \t\n\r\0\x0B";
134
135    private $dateFormats = ['F j, Y H:i', '%d days'];
136    private $numberFormat = [0, '.', ','];
137    private $timezone;
138
139    /**
140     * Sets the default format to be used by the date filter.
141     *
142     * @param string|null $format             The default date format string
143     * @param string|null $dateIntervalFormat The default date interval format string
144     */
145    public function setDateFormat($format = null, $dateIntervalFormat = null)
146    {
147        if (null !== $format) {
148            $this->dateFormats[0] = $format;
149        }
150
151        if (null !== $dateIntervalFormat) {
152            $this->dateFormats[1] = $dateIntervalFormat;
153        }
154    }
155
156    /**
157     * Gets the default format to be used by the date filter.
158     *
159     * @return array The default date format string and the default date interval format string
160     */
161    public function getDateFormat()
162    {
163        return $this->dateFormats;
164    }
165
166    /**
167     * Sets the default timezone to be used by the date filter.
168     *
169     * @param \DateTimeZone|string $timezone The default timezone string or a \DateTimeZone object
170     */
171    public function setTimezone($timezone)
172    {
173        $this->timezone = $timezone instanceof \DateTimeZone ? $timezone : new \DateTimeZone($timezone);
174    }
175
176    /**
177     * Gets the default timezone to be used by the date filter.
178     *
179     * @return \DateTimeZone The default timezone currently in use
180     */
181    public function getTimezone()
182    {
183        if (null === $this->timezone) {
184            $this->timezone = new \DateTimeZone(date_default_timezone_get());
185        }
186
187        return $this->timezone;
188    }
189
190    /**
191     * Sets the default format to be used by the number_format filter.
192     *
193     * @param int    $decimal      the number of decimal places to use
194     * @param string $decimalPoint the character(s) to use for the decimal point
195     * @param string $thousandSep  the character(s) to use for the thousands separator
196     */
197    public function setNumberFormat($decimal, $decimalPoint, $thousandSep)
198    {
199        $this->numberFormat = [$decimal, $decimalPoint, $thousandSep];
200    }
201
202    /**
203     * Get the default format used by the number_format filter.
204     *
205     * @return array The arguments for number_format()
206     */
207    public function getNumberFormat()
208    {
209        return $this->numberFormat;
210    }
211
212    public function getTokenParsers(): array
213    {
214        return [
215            new ApplyTokenParser(),
216            new ForTokenParser(),
217            new IfTokenParser(),
218            new ExtendsTokenParser(),
219            new IncludeTokenParser(),
220            new BlockTokenParser(),
221            new UseTokenParser(),
222            new MacroTokenParser(),
223            new ImportTokenParser(),
224            new FromTokenParser(),
225            new SetTokenParser(),
226            new TypesTokenParser(),
227            new FlushTokenParser(),
228            new DoTokenParser(),
229            new EmbedTokenParser(),
230            new WithTokenParser(),
231            new DeprecatedTokenParser(),
232            new GuardTokenParser(),
233        ];
234    }
235
236    public function getFilters(): array
237    {
238        return [
239            // formatting filters
240            new TwigFilter('date', [$this, 'formatDate']),
241            new TwigFilter('date_modify', [$this, 'modifyDate']),
242            new TwigFilter('format', [self::class, 'sprintf']),
243            new TwigFilter('replace', [self::class, 'replace']),
244            new TwigFilter('number_format', [$this, 'formatNumber']),
245            new TwigFilter('abs', 'abs'),
246            new TwigFilter('round', [self::class, 'round']),
247
248            // encoding
249            new TwigFilter('url_encode', [self::class, 'urlencode']),
250            new TwigFilter('json_encode', 'json_encode'),
251            new TwigFilter('convert_encoding', [self::class, 'convertEncoding']),
252
253            // string filters
254            new TwigFilter('title', [self::class, 'titleCase'], ['needs_charset' => true]),
255            new TwigFilter('capitalize', [self::class, 'capitalize'], ['needs_charset' => true]),
256            new TwigFilter('upper', [self::class, 'upper'], ['needs_charset' => true]),
257            new TwigFilter('lower', [self::class, 'lower'], ['needs_charset' => true]),
258            new TwigFilter('striptags', [self::class, 'striptags']),
259            new TwigFilter('trim', [self::class, 'trim']),
260            new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
261            new TwigFilter('spaceless', [self::class, 'spaceless'], ['pre_escape' => 'html', 'is_safe' => ['html'], 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12')]),
262
263            // array helpers
264            new TwigFilter('join', [self::class, 'join']),
265            new TwigFilter('split', [self::class, 'split'], ['needs_charset' => true]),
266            new TwigFilter('sort', [self::class, 'sort'], ['needs_environment' => true, 'needs_is_sandboxed' => true]),
267            new TwigFilter('merge', [self::class, 'merge']),
268            new TwigFilter('batch', [self::class, 'batch']),
269            new TwigFilter('column', [self::class, 'column'], ['needs_environment' => true, 'needs_is_sandboxed' => true]),
270            new TwigFilter('filter', [self::class, 'filter'], ['needs_environment' => true, 'needs_is_sandboxed' => true]),
271            new TwigFilter('map', [self::class, 'map'], ['needs_environment' => true, 'needs_is_sandboxed' => true]),
272            new TwigFilter('reduce', [self::class, 'reduce'], ['needs_environment' => true, 'needs_is_sandboxed' => true]),
273            new TwigFilter('find', [self::class, 'find'], ['needs_environment' => true, 'needs_is_sandboxed' => true]),
274
275            // string/array filters
276            new TwigFilter('reverse', [self::class, 'reverse'], ['needs_charset' => true]),
277            new TwigFilter('shuffle', [self::class, 'shuffle'], ['needs_charset' => true]),
278            new TwigFilter('length', [self::class, 'length'], ['needs_charset' => true]),
279            new TwigFilter('slice', [self::class, 'slice'], ['needs_charset' => true]),
280            new TwigFilter('first', [self::class, 'first'], ['needs_charset' => true]),
281            new TwigFilter('last', [self::class, 'last'], ['needs_charset' => true]),
282
283            // iteration and runtime
284            new TwigFilter('default', [self::class, 'default'], ['node_class' => DefaultFilter::class]),
285            new TwigFilter('keys', [self::class, 'keys']),
286            new TwigFilter('invoke', [self::class, 'invoke']),
287        ];
288    }
289
290    public function getFunctions(): array
291    {
292        return [
293            new TwigFunction('parent', null, ['parser_callable' => [self::class, 'parseParentFunction']]),
294            new TwigFunction('block', null, ['parser_callable' => [self::class, 'parseBlockFunction']]),
295            new TwigFunction('attribute', null, ['parser_callable' => [self::class, 'parseAttributeFunction']]),
296            new TwigFunction('max', 'max'),
297            new TwigFunction('min', 'min'),
298            new TwigFunction('range', 'range'),
299            new TwigFunction('constant', [self::class, 'constant']),
300            new TwigFunction('cycle', [self::class, 'cycle']),
301            new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]),
302            new TwigFunction('date', [$this, 'convertDate']),
303            new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]),
304            new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]),
305            new TwigFunction('enum_cases', [self::class, 'enumCases'], ['node_class' => EnumCasesFunction::class]),
306            new TwigFunction('enum', [self::class, 'enum'], ['node_class' => EnumFunction::class]),
307        ];
308    }
309
310    public function getTests(): array
311    {
312        return [
313            new TwigTest('even', null, ['node_class' => EvenTest::class]),
314            new TwigTest('odd', null, ['node_class' => OddTest::class]),
315            new TwigTest('defined', null, ['node_class' => DefinedTest::class]),
316            new TwigTest('same as', null, ['node_class' => SameasTest::class, 'one_mandatory_argument' => true]),
317            new TwigTest('none', null, ['node_class' => NullTest::class]),
318            new TwigTest('null', null, ['node_class' => NullTest::class]),
319            new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]),
320            new TwigTest('constant', null, ['node_class' => ConstantTest::class]),
321            new TwigTest('empty', [self::class, 'testEmpty']),
322            new TwigTest('iterable', 'is_iterable'),
323            new TwigTest('sequence', [self::class, 'testSequence']),
324            new TwigTest('mapping', [self::class, 'testMapping']),
325            new TwigTest('true', null, ['node_class' => TrueTest::class]),
326        ];
327    }
328
329    public function getNodeVisitors(): array
330    {
331        return [];
332    }
333
334    public function getExpressionParsers(): array
335    {
336        return [
337            // unary operators
338            new UnaryOperatorExpressionParser(NotUnary::class, 'not', 50, new PrecedenceChange('twig/twig', '3.15', 70)),
339            new UnaryOperatorExpressionParser(SpreadUnary::class, '...', 512, description: 'Spread operator', operandPrecedence: 0),
340            new UnaryOperatorExpressionParser(NegUnary::class, '-', 500),
341            new UnaryOperatorExpressionParser(PosUnary::class, '+', 500),
342
343            // binary operators
344            new BinaryOperatorExpressionParser(ElvisBinary::class, '?:', 5, InfixAssociativity::Right, description: 'Elvis operator (a ?: b)', aliases: ['? :']),
345            new BinaryOperatorExpressionParser(NullCoalesceBinary::class, '??', 300, InfixAssociativity::Right, new PrecedenceChange('twig/twig', '3.15', 5), description: 'Null coalescing operator (a ?? b)'),
346            new BinaryOperatorExpressionParser(OrBinary::class, 'or', 10),
347            new BinaryOperatorExpressionParser(XorBinary::class, 'xor', 12),
348            new BinaryOperatorExpressionParser(AndBinary::class, 'and', 15),
349            new BinaryOperatorExpressionParser(BitwiseOrBinary::class, 'b-or', 16),
350            new BinaryOperatorExpressionParser(BitwiseXorBinary::class, 'b-xor', 17),
351            new BinaryOperatorExpressionParser(BitwiseAndBinary::class, 'b-and', 18),
352            new BinaryOperatorExpressionParser(EqualBinary::class, '==', 20),
353            new BinaryOperatorExpressionParser(NotEqualBinary::class, '!=', 20),
354            new BinaryOperatorExpressionParser(SpaceshipBinary::class, '<=>', 20),
355            new BinaryOperatorExpressionParser(LessBinary::class, '<', 20),
356            new BinaryOperatorExpressionParser(GreaterBinary::class, '>', 20),
357            new BinaryOperatorExpressionParser(GreaterEqualBinary::class, '>=', 20),
358            new BinaryOperatorExpressionParser(LessEqualBinary::class, '<=', 20),
359            new BinaryOperatorExpressionParser(NotInBinary::class, 'not in', 20),
360            new BinaryOperatorExpressionParser(InBinary::class, 'in', 20),
361            new BinaryOperatorExpressionParser(MatchesBinary::class, 'matches', 20),
362            new BinaryOperatorExpressionParser(StartsWithBinary::class, 'starts with', 20),
363            new BinaryOperatorExpressionParser(EndsWithBinary::class, 'ends with', 20),
364            new BinaryOperatorExpressionParser(HasSomeBinary::class, 'has some', 20),
365            new BinaryOperatorExpressionParser(HasEveryBinary::class, 'has every', 20),
366            new BinaryOperatorExpressionParser(SameAsBinary::class, '===', 20),
367            new BinaryOperatorExpressionParser(NotSameAsBinary::class, '!==', 20),
368            new BinaryOperatorExpressionParser(RangeBinary::class, '..', 25),
369            new BinaryOperatorExpressionParser(AddBinary::class, '+', 30),
370            new BinaryOperatorExpressionParser(SubBinary::class, '-', 30),
371            new BinaryOperatorExpressionParser(ConcatBinary::class, '~', 40, precedenceChange: new PrecedenceChange('twig/twig', '3.15', 27)),
372            new BinaryOperatorExpressionParser(MulBinary::class, '*', 60),
373            new BinaryOperatorExpressionParser(DivBinary::class, '/', 60),
374            new BinaryOperatorExpressionParser(FloorDivBinary::class, '//', 60, description: 'Floor division'),
375            new BinaryOperatorExpressionParser(ModBinary::class, '%', 60),
376            new BinaryOperatorExpressionParser(PowerBinary::class, '**', 200, InfixAssociativity::Right, description: 'Exponentiation operator'),
377
378            // ternary operator
379            new ConditionalTernaryExpressionParser(),
380
381            // assignment operator
382            new AssignmentExpressionParser('='),
383
384            // Twig callables
385            new IsExpressionParser(),
386            new IsNotExpressionParser(),
387            new FilterExpressionParser(),
388            new FunctionExpressionParser(),
389
390            // get attribute operators
391            new DotExpressionParser(),
392            new SquareBracketExpressionParser(),
393
394            // group expression
395            new GroupingExpressionParser(),
396
397            // arrow function
398            new ArrowExpressionParser(),
399
400            // all literals
401            new LiteralExpressionParser(),
402        ];
403    }
404
405    /**
406     * Cycles over a sequence.
407     *
408     * @param array|\ArrayAccess $values   A non-empty sequence of values
409     * @param int<0, max>        $position The position of the value to return in the cycle
410     *
411     * @return mixed The value at the given position in the sequence, wrapping around as needed
412     *
413     * @internal
414     */
415    public static function cycle($values, $position): mixed
416    {
417        if (!\is_array($values)) {
418            if (!$values instanceof \ArrayAccess) {
419                throw new RuntimeError('The "cycle" function expects an array or "ArrayAccess" as first argument.');
420            }
421
422            if (!is_countable($values)) {
423                // To be uncommented in 4.0
424                // throw new RuntimeError('The "cycle" function expects a countable sequence as first argument.');
425
426                trigger_deprecation('twig/twig', '3.12', 'Passing a non-countable sequence of values to "%s()" is deprecated.', __METHOD__);
427
428                $values = self::toArray($values, false);
429            }
430        }
431
432        if (!$count = \count($values)) {
433            throw new RuntimeError('The "cycle" function expects a non-empty sequence.');
434        }
435
436        return $values[$position % $count];
437    }
438
439    /**
440     * Returns a random value depending on the supplied parameter type:
441     * - a random item from a \Traversable or array
442     * - a random character from a string
443     * - a random integer between 0 and the integer parameter.
444     *
445     * @param \Traversable|array|int|float|string $values The values to pick a random item from
446     * @param int|null                            $max    Maximum value used when $values is an int
447     *
448     * @return mixed A random value from the given sequence
449     *
450     * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is)
451     *
452     * @internal
453     */
454    public static function random(string $charset, $values = null, $max = null)
455    {
456        if (null === $values) {
457            return null === $max ? mt_rand() : mt_rand(0, (int) $max);
458        }
459
460        if (\is_int($values) || \is_float($values)) {
461            if (null === $max) {
462                if ($values < 0) {
463                    $max = 0;
464                    $min = $values;
465                } else {
466                    $max = $values;
467                    $min = 0;
468                }
469            } else {
470                $min = $values;
471            }
472
473            return mt_rand((int) $min, (int) $max);
474        }
475
476        if (\is_string($values)) {
477            if ('' === $values) {
478                return '';
479            }
480
481            if ('UTF-8' !== $charset) {
482                $values = self::convertEncoding($values, 'UTF-8', $charset);
483            }
484
485            // unicode version of str_split()
486            // split at all positions, but not after the start and not before the end
487            $values = preg_split('/(?<!^)(?!$)/u', $values);
488
489            if ('UTF-8' !== $charset) {
490                foreach ($values as $i => $value) {
491                    $values[$i] = self::convertEncoding($value, $charset, 'UTF-8');
492                }
493            }
494        }
495
496        if (!is_iterable($values)) {
497            return $values;
498        }
499
500        $values = self::toArray($values);
501
502        if (0 === \count($values)) {
503            throw new RuntimeError('The "random" function cannot pick from an empty sequence or mapping.');
504        }
505
506        return $values[array_rand($values, 1)];
507    }
508
509    /**
510     * Formats a date.
511     *
512     *   {{ post.published_at|date("m/d/Y") }}
513     *
514     * @param \DateTimeInterface|\DateInterval|string|int|null $date     A date, a timestamp or null to use the current time
515     * @param string|null                                      $format   The target format, null to use the default
516     * @param \DateTimeZone|string|false|null                  $timezone The target timezone, null to use the default, false to leave unchanged
517     */
518    public function formatDate($date, $format = null, $timezone = null): string
519    {
520        if (null === $format) {
521            $formats = $this->getDateFormat();
522            $format = $date instanceof \DateInterval ? $formats[1] : $formats[0];
523        }
524
525        if ($date instanceof \DateInterval) {
526            return $date->format($format);
527        }
528
529        return $this->convertDate($date, $timezone)->format($format);
530    }
531
532    /**
533     * Returns a new date object modified.
534     *
535     *   {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
536     *
537     * @param \DateTimeInterface|string|int|null $date     A date, a timestamp or null to use the current time
538     * @param string                             $modifier A modifier string
539     *
540     * @return \DateTime|\DateTimeImmutable
541     *
542     * @internal
543     */
544    public function modifyDate($date, $modifier)
545    {
546        return $this->convertDate($date, false)->modify($modifier);
547    }
548
549    /**
550     * Returns a formatted string.
551     *
552     * @param string|null $format
553     *
554     * @internal
555     */
556    public static function sprintf($format, ...$values): string
557    {
558        return \sprintf($format ?? '', ...$values);
559    }
560
561    /**
562     * @internal
563     */
564    public static function dateConverter(Environment $env, $date, $format = null, $timezone = null): string
565    {
566        return $env->getExtension(self::class)->formatDate($date, $format, $timezone);
567    }
568
569    /**
570     * Converts an input to a \DateTime instance.
571     *
572     *    {% if date(user.created_at) < date('+2days') %}
573     *      {# do something #}
574     *    {% endif %}
575     *
576     * @param \DateTimeInterface|string|int|null $date     A date, a timestamp or null to use the current time
577     * @param \DateTimeZone|string|false|null    $timezone The target timezone, null to use the default, false to leave unchanged
578     *
579     * @return \DateTime|\DateTimeImmutable
580     */
581    public function convertDate($date = null, $timezone = null)
582    {
583        // determine the timezone
584        if (false !== $timezone) {
585            if (null === $timezone) {
586                $timezone = $this->getTimezone();
587            } elseif (!$timezone instanceof \DateTimeZone) {
588                $timezone = new \DateTimeZone($timezone);
589            }
590        }
591
592        // immutable dates
593        if ($date instanceof \DateTimeImmutable) {
594            return false !== $timezone ? $date->setTimezone($timezone) : $date;
595        }
596
597        if ($date instanceof \DateTime) {
598            $date = clone $date;
599            if (false !== $timezone) {
600                $date->setTimezone($timezone);
601            }
602
603            return $date;
604        }
605
606        if (null === $date || 'now' === $date) {
607            if (null === $date) {
608                $date = 'now';
609            }
610
611            return new \DateTime($date, false !== $timezone ? $timezone : $this->getTimezone());
612        }
613
614        $asString = (string) $date;
615        if (ctype_digit($asString) || ('' !== $asString && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
616            $date = new \DateTime('@'.$date);
617        } else {
618            $date = new \DateTime($date);
619        }
620
621        if (false !== $timezone) {
622            $date->setTimezone($timezone);
623        }
624
625        return $date;
626    }
627
628    /**
629     * Replaces strings within a string.
630     *
631     * @param string|null        $str  String to replace in
632     * @param array|\Traversable $from Replace values
633     *
634     * @internal
635     */
636    public static function replace($str, $from): string
637    {
638        if (!is_iterable($from)) {
639            throw new RuntimeError(\sprintf('The "replace" filter expects a sequence or a mapping, got "%s".', get_debug_type($from)));
640        }
641
642        return strtr($str ?? '', self::toArray($from));
643    }
644
645    /**
646     * Rounds a number.
647     *
648     * @param int|float|string|null   $value     The value to round
649     * @param int|float               $precision The rounding precision
650     * @param 'common'|'ceil'|'floor' $method    The method to use for rounding
651     *
652     * @return float The rounded number
653     *
654     * @internal
655     */
656    public static function round($value, $precision = 0, $method = 'common')
657    {
658        $value = (float) $value;
659
660        if ('common' === $method) {
661            return round($value, $precision);
662        }
663
664        if ('ceil' !== $method && 'floor' !== $method) {
665            throw new RuntimeError('The "round" filter only supports the "common", "ceil", and "floor" methods.');
666        }
667
668        return $method($value * 10 ** $precision) / 10 ** $precision;
669    }
670
671    /**
672     * Formats a number.
673     *
674     * All of the formatting options can be left null, in that case the defaults will
675     * be used. Supplying any of the parameters will override the defaults set in the
676     * environment object.
677     *
678     * @param mixed       $number       A float/int/string of the number to format
679     * @param int|null    $decimal      the number of decimal points to display
680     * @param string|null $decimalPoint the character(s) to use for the decimal point
681     * @param string|null $thousandSep  the character(s) to use for the thousands separator
682     */
683    public function formatNumber($number, $decimal = null, $decimalPoint = null, $thousandSep = null): string
684    {
685        $defaults = $this->getNumberFormat();
686        if (null === $decimal) {
687            $decimal = $defaults[0];
688        }
689
690        if (null === $decimalPoint) {
691            $decimalPoint = $defaults[1];
692        }
693
694        if (null === $thousandSep) {
695            $thousandSep = $defaults[2];
696        }
697
698        return number_format((float) $number, $decimal, $decimalPoint, $thousandSep);
699    }
700
701    /**
702     * URL encodes (RFC 3986) a string as a path segment or an array as a query string.
703     *
704     * @param string|array|null $url A URL or an array of query parameters
705     *
706     * @internal
707     */
708    public static function urlencode($url): string
709    {
710        if (\is_array($url)) {
711            return http_build_query($url, '', '&', \PHP_QUERY_RFC3986);
712        }
713
714        return rawurlencode($url ?? '');
715    }
716
717    /**
718     * Merges any number of arrays or Traversable objects.
719     *
720     *  {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
721     *
722     *  {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %}
723     *
724     *  {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #}
725     *
726     * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge
727     *
728     * @internal
729     */
730    public static function merge(...$arrays): array
731    {
732        $result = [];
733
734        foreach ($arrays as $argNumber => $array) {
735            if (!is_iterable($array)) {
736                throw new RuntimeError(\sprintf('The "merge" filter expects a sequence or a mapping, got "%s" for argument %d.', get_debug_type($array), $argNumber + 1));
737            }
738
739            $result = array_merge($result, self::toArray($array));
740        }
741
742        return $result;
743    }
744
745    /**
746     * Slices a variable.
747     *
748     * @param mixed $item         A variable
749     * @param int   $start        Start of the slice
750     * @param int   $length       Size of the slice
751     * @param bool  $preserveKeys Whether to preserve key or not (when the input is an array)
752     *
753     * @return mixed The sliced variable
754     *
755     * @internal
756     */
757    public static function slice(string $charset, $item, $start, $length = null, $preserveKeys = false)
758    {
759        if ($item instanceof \Traversable) {
760            while ($item instanceof \IteratorAggregate) {
761                $item = $item->getIterator();
762            }
763
764            if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) {
765                try {
766                    return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys);
767                } catch (\OutOfBoundsException $e) {
768                    return [];
769                }
770            }
771
772            $item = iterator_to_array($item, $preserveKeys);
773        }
774
775        if (\is_array($item)) {
776            return \array_slice($item, $start, $length, $preserveKeys);
777        }
778
779        return mb_substr((string) $item, $start, $length, $charset);
780    }
781
782    /**
783     * Returns the first element of the item.
784     *
785     * @param mixed $item A variable
786     *
787     * @return mixed The first element of the item
788     *
789     * @internal
790     */
791    public static function first(string $charset, $item)
792    {
793        $elements = self::slice($charset, $item, 0, 1, false);
794
795        return \is_string($elements) ? $elements : current($elements);
796    }
797
798    /**
799     * Returns the last element of the item.
800     *
801     * @param mixed $item A variable
802     *
803     * @return mixed The last element of the item
804     *
805     * @internal
806     */
807    public static function last(string $charset, $item)
808    {
809        $elements = self::slice($charset, $item, -1, 1, false);
810
811        return \is_string($elements) ? $elements : current($elements);
812    }
813
814    /**
815     * Joins the values to a string.
816     *
817     * The separators between elements are empty strings per default, you can define them with the optional parameters.
818     *
819     *  {{ [1, 2, 3]|join(', ', ' and ') }}
820     *  {# returns 1, 2 and 3 #}
821     *
822     *  {{ [1, 2, 3]|join('|') }}
823     *  {# returns 1|2|3 #}
824     *
825     *  {{ [1, 2, 3]|join }}
826     *  {# returns 123 #}
827     *
828     * @param iterable|array|string|float|int|bool|null $value An array
829     * @param string                                    $glue  The separator
830     * @param string|null                               $and   The separator for the last pair
831     *
832     * @internal
833     */
834    public static function join($value, $glue = '', $and = null): string
835    {
836        if (!is_iterable($value)) {
837            $value = (array) $value;
838        }
839
840        $value = self::toArray($value, false);
841
842        if (0 === \count($value)) {
843            return '';
844        }
845
846        if (null === $and || $and === $glue) {
847            return implode($glue, $value);
848        }
849
850        if (1 === \count($value)) {
851            return $value[0];
852        }
853
854        return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1];
855    }
856
857    /**
858     * Splits the string into an array.
859     *
860     *  {{ "one,two,three"|split(',') }}
861     *  {# returns [one, two, three] #}
862     *
863     *  {{ "one,two,three,four,five"|split(',', 3) }}
864     *  {# returns [one, two, "three,four,five"] #}
865     *
866     *  {{ "123"|split('') }}
867     *  {# returns [1, 2, 3] #}
868     *
869     *  {{ "aabbcc"|split('', 2) }}
870     *  {# returns [aa, bb, cc] #}
871     *
872     * @param string|null $value     A string
873     * @param string      $delimiter The delimiter
874     * @param int|null    $limit     The limit
875     *
876     * @internal
877     */
878    public static function split(string $charset, $value, $delimiter, $limit = null): array
879    {
880        $value = $value ?? '';
881
882        if ('' !== $delimiter) {
883            return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
884        }
885
886        if ($limit <= 1) {
887            return preg_split('/(?<!^)(?!$)/u', $value);
888        }
889
890        $length = mb_strlen($value, $charset);
891        if ($length < $limit) {
892            return [$value];
893        }
894
895        $r = [];
896        for ($i = 0; $i < $length; $i += $limit) {
897            $r[] = mb_substr($value, $i, $limit, $charset);
898        }
899
900        return $r;
901    }
902
903    /**
904     * @internal
905     */
906    public static function default($value, $default = '')
907    {
908        if (self::testEmpty($value)) {
909            return $default;
910        }
911
912        return $value;
913    }
914
915    /**
916     * Returns the keys for the given array.
917     *
918     * It is useful when you want to iterate over the keys of an array:
919     *
920     *  {% for key in array|keys %}
921     *      {# ... #}
922     *  {% endfor %}
923     *
924     * @internal
925     */
926    public static function keys($array): array
927    {
928        if ($array instanceof \Traversable) {
929            while ($array instanceof \IteratorAggregate) {
930                $array = $array->getIterator();
931            }
932
933            $keys = [];
934            if ($array instanceof \Iterator) {
935                $array->rewind();
936                while ($array->valid()) {
937                    $keys[] = $array->key();
938                    $array->next();
939                }
940
941                return $keys;
942            }
943
944            foreach ($array as $key => $item) {
945                $keys[] = $key;
946            }
947
948            return $keys;
949        }
950
951        if (!\is_array($array)) {
952            return [];
953        }
954
955        return array_keys($array);
956    }
957
958    /**
959     * Invokes a callable.
960     *
961     * @internal
962     */
963    public static function invoke(\Closure $arrow, ...$arguments): mixed
964    {
965        return $arrow(...$arguments);
966    }
967
968    /**
969     * Reverses a variable.
970     *
971     * @param array|\Traversable|string|null $item         An array, a \Traversable instance, or a string
972     * @param bool                           $preserveKeys Whether to preserve key or not
973     *
974     * @return mixed The reversed input
975     *
976     * @internal
977     */
978    public static function reverse(string $charset, $item, $preserveKeys = false)
979    {
980        if ($item instanceof \Traversable) {
981            return array_reverse(iterator_to_array($item), $preserveKeys);
982        }
983
984        if (\is_array($item)) {
985            return array_reverse($item, $preserveKeys);
986        }
987
988        $string = (string) $item;
989
990        if ('UTF-8' !== $charset) {
991            $string = self::convertEncoding($string, 'UTF-8', $charset);
992        }
993
994        preg_match_all('/./us', $string, $matches);
995
996        $string = implode('', array_reverse($matches[0]));
997
998        if ('UTF-8' !== $charset) {
999            $string = self::convertEncoding($string, $charset, 'UTF-8');
1000        }
1001
1002        return $string;
1003    }
1004
1005    /**
1006     * Shuffles an array, a \Traversable instance, or a string.
1007     * The function does not preserve keys.
1008     *
1009     * @param array|\Traversable|string|null $item
1010     *
1011     * @internal
1012     */
1013    public static function shuffle(string $charset, $item)
1014    {
1015        if (\is_string($item)) {
1016            if ('UTF-8' !== $charset) {
1017                $item = self::convertEncoding($item, 'UTF-8', $charset);
1018            }
1019
1020            $item = preg_split('/(?<!^)(?!$)/u', $item, -1);
1021            shuffle($item);
1022            $item = implode('', $item);
1023
1024            if ('UTF-8' !== $charset) {
1025                $item = self::convertEncoding($item, $charset, 'UTF-8');
1026            }
1027
1028            return $item;
1029        }
1030
1031        if (is_iterable($item)) {
1032            $item = self::toArray($item, false);
1033            shuffle($item);
1034        }
1035
1036        return $item;
1037    }
1038
1039    /**
1040     * Sorts an array.
1041     *
1042     * @param array|\Traversable $array
1043     * @param ?\Closure          $arrow
1044     *
1045     * @internal
1046     */
1047    public static function sort(Environment $env, bool $isSandboxed, $array, $arrow = null): array
1048    {
1049        if ($array instanceof \Traversable) {
1050            $array = iterator_to_array($array);
1051        } elseif (!\is_array($array)) {
1052            throw new RuntimeError(\sprintf('The "sort" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
1053        }
1054
1055        if (null !== $arrow) {
1056            self::checkArrow($isSandboxed, $arrow, 'sort', 'filter');
1057
1058            uasort($array, $arrow);
1059        } else {
1060            asort($array);
1061        }
1062
1063        return $array;
1064    }
1065
1066    /**
1067     * @internal
1068     */
1069    public static function inFilter($value, $compare)
1070    {
1071        if ($value instanceof Markup) {
1072            $value = (string) $value;
1073        }
1074        if ($compare instanceof Markup) {
1075            $compare = (string) $compare;
1076        }
1077
1078        if (\is_string($compare)) {
1079            if (\is_string($value) || \is_int($value) || \is_float($value)) {
1080                return '' === $value || str_contains($compare, (string) $value);
1081            }
1082
1083            return false;
1084        }
1085
1086        if (!is_iterable($compare)) {
1087            return false;
1088        }
1089
1090        if (\is_object($value) || \is_resource($value)) {
1091            if (!\is_array($compare)) {
1092                foreach ($compare as $item) {
1093                    if ($item === $value) {
1094                        return true;
1095                    }
1096                }
1097
1098                return false;
1099            }
1100
1101            return \in_array($value, $compare, true);
1102        }
1103
1104        foreach ($compare as $item) {
1105            if (0 === self::compare($value, $item)) {
1106                return true;
1107            }
1108        }
1109
1110        return false;
1111    }
1112
1113    /**
1114     * Compares two values using a more strict version of the PHP non-strict comparison operator.
1115     *
1116     * @see https://wiki.php.net/rfc/string_to_number_comparison
1117     * @see https://wiki.php.net/rfc/trailing_whitespace_numerics
1118     *
1119     * @internal
1120     */
1121    public static function compare($a, $b)
1122    {
1123        // int <=> string
1124        if (\is_int($a) && \is_string($b)) {
1125            $bTrim = trim($b, " \t\n\r\v\f");
1126            if (!is_numeric($bTrim)) {
1127                return (string) $a <=> $b;
1128            }
1129            if ((int) $bTrim == $bTrim) {
1130                return $a <=> (int) $bTrim;
1131            }
1132
1133            return (float) $a <=> (float) $bTrim;
1134        }
1135        if (\is_string($a) && \is_int($b)) {
1136            $aTrim = trim($a, " \t\n\r\v\f");
1137            if (!is_numeric($aTrim)) {
1138                return $a <=> (string) $b;
1139            }
1140            if ((int) $aTrim == $aTrim) {
1141                return (int) $aTrim <=> $b;
1142            }
1143
1144            return (float) $aTrim <=> (float) $b;
1145        }
1146
1147        // float <=> string
1148        if (\is_float($a) && \is_string($b)) {
1149            if (is_nan($a)) {
1150                return 1;
1151            }
1152            $bTrim = trim($b, " \t\n\r\v\f");
1153            if (!is_numeric($bTrim)) {
1154                return (string) $a <=> $b;
1155            }
1156
1157            return $a <=> (float) $bTrim;
1158        }
1159        if (\is_string($a) && \is_float($b)) {
1160            if (is_nan($b)) {
1161                return 1;
1162            }
1163            $aTrim = trim($a, " \t\n\r\v\f");
1164            if (!is_numeric($aTrim)) {
1165                return $a <=> (string) $b;
1166            }
1167
1168            return (float) $aTrim <=> $b;
1169        }
1170
1171        // fallback to <=>
1172        return $a <=> $b;
1173    }
1174
1175    /**
1176     * @throws RuntimeError When an invalid pattern is used
1177     *
1178     * @internal
1179     */
1180    public static function matches(string $regexp, ?string $str): int
1181    {
1182        set_error_handler(static function ($t, $m) use ($regexp) {
1183            throw new RuntimeError(\sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12));
1184        });
1185        try {
1186            return preg_match($regexp, $str ?? '');
1187        } finally {
1188            restore_error_handler();
1189        }
1190    }
1191
1192    /**
1193     * Returns a trimmed string.
1194     *
1195     * @param string|\Stringable|null $string
1196     * @param string|null             $characterMask
1197     * @param string                  $side          left, right, or both
1198     *
1199     * @throws RuntimeError When an invalid trimming side is used
1200     *
1201     * @internal
1202     */
1203    public static function trim($string, $characterMask = null, $side = 'both'): string|\Stringable
1204    {
1205        if (null === $characterMask) {
1206            $characterMask = self::DEFAULT_TRIM_CHARS;
1207        }
1208
1209        $trimmed = match ($side) {
1210            'both' => trim($string ?? '', $characterMask),
1211            'left' => ltrim($string ?? '', $characterMask),
1212            'right' => rtrim($string ?? '', $characterMask),
1213            default => throw new RuntimeError('Trimming side must be "left", "right" or "both".'),
1214        };
1215
1216        // trimming a safe string with the default character mask always returns a safe string (independently of the context)
1217        return $string instanceof Markup && self::DEFAULT_TRIM_CHARS === $characterMask ? new Markup($trimmed, $string->getCharset()) : $trimmed;
1218    }
1219
1220    /**
1221     * Inserts HTML line breaks before all newlines in a string.
1222     *
1223     * @param string|null $string
1224     *
1225     * @internal
1226     */
1227    public static function nl2br($string): string
1228    {
1229        return nl2br($string ?? '');
1230    }
1231
1232    /**
1233     * Removes whitespaces between HTML tags.
1234     *
1235     * @param string|null $content
1236     *
1237     * @internal
1238     */
1239    public static function spaceless($content): string
1240    {
1241        return trim(preg_replace('/>\s+</', '><', $content ?? ''));
1242    }
1243
1244    /**
1245     * @param string|null $string
1246     * @param string      $to
1247     * @param string      $from
1248     *
1249     * @internal
1250     */
1251    public static function convertEncoding($string, $to, $from): string
1252    {
1253        if (!\function_exists('iconv')) {
1254            throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.');
1255        }
1256
1257        return iconv($from, $to, $string ?? '');
1258    }
1259
1260    /**
1261     * Returns the length of a variable.
1262     *
1263     * @param mixed $thing A variable
1264     *
1265     * @internal
1266     */
1267    public static function length(string $charset, $thing): int
1268    {
1269        if (null === $thing) {
1270            return 0;
1271        }
1272
1273        if (\is_scalar($thing)) {
1274            return mb_strlen($thing, $charset);
1275        }
1276
1277        if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) {
1278            return \count($thing);
1279        }
1280
1281        if ($thing instanceof \Traversable) {
1282            return iterator_count($thing);
1283        }
1284
1285        if ($thing instanceof \Stringable) {
1286            return mb_strlen((string) $thing, $charset);
1287        }
1288
1289        return 1;
1290    }
1291
1292    /**
1293     * Converts a string to uppercase.
1294     *
1295     * @param string|null $string A string
1296     *
1297     * @internal
1298     */
1299    public static function upper(string $charset, $string): string
1300    {
1301        return mb_strtoupper($string ?? '', $charset);
1302    }
1303
1304    /**
1305     * Converts a string to lowercase.
1306     *
1307     * @param string|null $string A string
1308     *
1309     * @internal
1310     */
1311    public static function lower(string $charset, $string): string
1312    {
1313        return mb_strtolower($string ?? '', $charset);
1314    }
1315
1316    /**
1317     * Strips HTML and PHP tags from a string.
1318     *
1319     * @param string|null          $string
1320     * @param string[]|string|null $allowable_tags
1321     *
1322     * @internal
1323     */
1324    public static function striptags($string, $allowable_tags = null): string
1325    {
1326        return strip_tags($string ?? '', $allowable_tags);
1327    }
1328
1329    /**
1330     * Returns a titlecased string.
1331     *
1332     * @param string|null $string A string
1333     *
1334     * @internal
1335     */
1336    public static function titleCase(string $charset, $string): string
1337    {
1338        return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset);
1339    }
1340
1341    /**
1342     * Returns a capitalized string.
1343     *
1344     * @param string|null $string A string
1345     *
1346     * @internal
1347     */
1348    public static function capitalize(string $charset, $string): string
1349    {
1350        return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset);
1351    }
1352
1353    /**
1354     * @internal
1355     *
1356     * to be removed in 4.0
1357     */
1358    public static function callMacro(Template $template, string $method, array $args, int $lineno, array $context, Source $source)
1359    {
1360        if (!method_exists($template, $method)) {
1361            $parent = $template;
1362            while ($parent = $parent->getParent($context)) {
1363                if (method_exists($parent, $method)) {
1364                    return $parent->$method(...$args);
1365                }
1366            }
1367
1368            throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source);
1369        }
1370
1371        return $template->$method(...$args);
1372    }
1373
1374    /**
1375     * @template TSequence
1376     *
1377     * @param TSequence $seq
1378     *
1379     * @return ($seq is iterable ? TSequence : array{})
1380     *
1381     * @internal
1382     */
1383    public static function ensureTraversable($seq)
1384    {
1385        if (is_iterable($seq)) {
1386            return $seq;
1387        }
1388
1389        return [];
1390    }
1391
1392    /**
1393     * @internal
1394     */
1395    public static function toArray($seq, $preserveKeys = true)
1396    {
1397        if ($seq instanceof \Traversable) {
1398            return iterator_to_array($seq, $preserveKeys);
1399        }
1400
1401        if (!\is_array($seq)) {
1402            return $seq;
1403        }
1404
1405        return $preserveKeys ? $seq : array_values($seq);
1406    }
1407
1408    /**
1409     * Checks if a variable is empty.
1410     *
1411     *    {# evaluates to true if the foo variable is null, false, or the empty string #}
1412     *    {% if foo is empty %}
1413     *        {# ... #}
1414     *    {% endif %}
1415     *
1416     * @param mixed $value A variable
1417     *
1418     * @internal
1419     */
1420    public static function testEmpty($value): bool
1421    {
1422        if ($value instanceof \Countable) {
1423            return 0 === \count($value);
1424        }
1425
1426        if ($value instanceof \Traversable) {
1427            return !iterator_count($value);
1428        }
1429
1430        if ($value instanceof \Stringable) {
1431            return '' === (string) $value;
1432        }
1433
1434        return '' === $value || false === $value || null === $value || [] === $value;
1435    }
1436
1437    /**
1438     * Checks if a variable is a sequence.
1439     *
1440     *    {# evaluates to true if the foo variable is a sequence #}
1441     *    {% if foo is sequence %}
1442     *        {# ... #}
1443     *    {% endif %}
1444     *
1445     * @internal
1446     */
1447    public static function testSequence($value): bool
1448    {
1449        if ($value instanceof \ArrayObject) {
1450            $value = $value->getArrayCopy();
1451        }
1452
1453        if ($value instanceof \Traversable) {
1454            $value = iterator_to_array($value);
1455        }
1456
1457        return \is_array($value) && array_is_list($value);
1458    }
1459
1460    /**
1461     * Checks if a variable is a mapping.
1462     *
1463     *    {# evaluates to true if the foo variable is a mapping #}
1464     *    {% if foo is mapping %}
1465     *        {# ... #}
1466     *    {% endif %}
1467     *
1468     * @internal
1469     */
1470    public static function testMapping($value): bool
1471    {
1472        if ($value instanceof \ArrayObject) {
1473            $value = $value->getArrayCopy();
1474        }
1475
1476        if ($value instanceof \Traversable) {
1477            $value = iterator_to_array($value);
1478        }
1479
1480        return (\is_array($value) && !array_is_list($value)) || \is_object($value);
1481    }
1482
1483    /**
1484     * Renders a template.
1485     *
1486     * @param array                        $context
1487     * @param string|array|TemplateWrapper $template      The template to render or an array of templates to try consecutively
1488     * @param array                        $variables     The variables to pass to the template
1489     * @param bool                         $withContext
1490     * @param bool                         $ignoreMissing Whether to ignore missing templates or not
1491     * @param bool                         $sandboxed     Whether to sandbox the template or not
1492     *
1493     * @internal
1494     */
1495    public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false): string
1496    {
1497        $alreadySandboxed = false;
1498        $sandbox = null;
1499        if ($withContext) {
1500            $variables = array_merge($context, $variables);
1501        }
1502
1503        if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) {
1504            $sandbox = $env->getExtension(SandboxExtension::class);
1505            if (!$alreadySandboxed = $sandbox->isSandboxed()) {
1506                $sandbox->enableSandbox();
1507            }
1508        }
1509
1510        try {
1511            $loaded = null;
1512            try {
1513                $loaded = $env->resolveTemplate($template);
1514            } catch (LoaderError $e) {
1515                if (!$ignoreMissing) {
1516                    throw $e;
1517                }
1518
1519                return '';
1520            }
1521
1522            return $loaded->render($variables);
1523        } finally {
1524            if ($isSandboxed && !$alreadySandboxed) {
1525                $sandbox->disableSandbox();
1526            }
1527        }
1528    }
1529
1530    /**
1531     * Returns a template content without rendering it.
1532     *
1533     * @param string $name          The template name
1534     * @param bool   $ignoreMissing Whether to ignore missing templates or not
1535     *
1536     * @internal
1537     */
1538    public static function source(Environment $env, $name, $ignoreMissing = false): string
1539    {
1540        $loader = $env->getLoader();
1541        try {
1542            return $loader->getSourceContext($name)->getCode();
1543        } catch (LoaderError $e) {
1544            if (!$ignoreMissing) {
1545                throw $e;
1546            }
1547
1548            return '';
1549        }
1550    }
1551
1552    /**
1553     * Returns the list of cases of the enum.
1554     *
1555     * @template T of \UnitEnum
1556     *
1557     * @param class-string<T> $enum
1558     *
1559     * @return list<T>
1560     *
1561     * @internal
1562     */
1563    public static function enumCases(string $enum): array
1564    {
1565        if (!enum_exists($enum)) {
1566            throw new RuntimeError(\sprintf('Enum "%s" does not exist.', $enum));
1567        }
1568
1569        return $enum::cases();
1570    }
1571
1572    /**
1573     * Provides the ability to access enums by their class names.
1574     *
1575     * @template T of \UnitEnum
1576     *
1577     * @param class-string<T> $enum
1578     *
1579     * @return T
1580     *
1581     * @internal
1582     */
1583    public static function enum(string $enum): \UnitEnum
1584    {
1585        if (!enum_exists($enum)) {
1586            throw new RuntimeError(\sprintf('"%s" is not an enum.', $enum));
1587        }
1588
1589        if (!$cases = $enum::cases()) {
1590            throw new RuntimeError(\sprintf('"%s" is an empty enum.', $enum));
1591        }
1592
1593        return $cases[0];
1594    }
1595
1596    /**
1597     * Provides the ability to get constants from instances as well as class/global constants.
1598     *
1599     * @param string      $constant     The name of the constant
1600     * @param object|null $object       The object to get the constant from
1601     * @param bool        $checkDefined Whether to check if the constant is defined or not
1602     *
1603     * @return mixed Class constants can return many types like scalars, arrays, and
1604     *               objects depending on the PHP version (\BackedEnum, \UnitEnum, etc.)
1605     *               When $checkDefined is true, returns true when the constant is defined, false otherwise
1606     *
1607     * @internal
1608     */
1609    public static function constant($constant, $object = null, bool $checkDefined = false)
1610    {
1611        if (null !== $object) {
1612            if ('class' === $constant) {
1613                return $checkDefined ? true : $object::class;
1614            }
1615
1616            $constant = $object::class.'::'.$constant;
1617        }
1618
1619        if (!\defined($constant)) {
1620            if ($checkDefined) {
1621                return false;
1622            }
1623
1624            if ('::class' === strtolower(substr($constant, -7))) {
1625                throw new RuntimeError(\sprintf('You cannot use the Twig function "constant" to access "%s". You could provide an object and call constant("class", $object) or use the class name directly as a string.', $constant));
1626            }
1627
1628            throw new RuntimeError(\sprintf('Constant "%s" is undefined.', $constant));
1629        }
1630
1631        return $checkDefined ? true : \constant($constant);
1632    }
1633
1634    /**
1635     * Batches item.
1636     *
1637     * @param array $items An array of items
1638     * @param int   $size  The size of the batch
1639     * @param mixed $fill  A value used to fill missing items
1640     *
1641     * @internal
1642     */
1643    public static function batch($items, $size, $fill = null, $preserveKeys = true): array
1644    {
1645        if (!is_iterable($items)) {
1646            throw new RuntimeError(\sprintf('The "batch" filter expects a sequence or a mapping, got "%s".', get_debug_type($items)));
1647        }
1648
1649        $size = (int) ceil($size);
1650
1651        $result = array_chunk(self::toArray($items, $preserveKeys), $size, $preserveKeys);
1652
1653        if (null !== $fill && $result) {
1654            $last = \count($result) - 1;
1655            if ($fillCount = $size - \count($result[$last])) {
1656                for ($i = 0; $i < $fillCount; ++$i) {
1657                    $result[$last][] = $fill;
1658                }
1659            }
1660        }
1661
1662        return $result;
1663    }
1664
1665    /**
1666     * Returns the attribute value for a given array/object.
1667     *
1668     * @param mixed  $object            The object or array from where to get the item
1669     * @param mixed  $item              The item to get from the array or object
1670     * @param array  $arguments         An array of arguments to pass if the item is an object method
1671     * @param string $type              The type of attribute (@see \Twig\Template constants)
1672     * @param bool   $isDefinedTest     Whether this is only a defined check
1673     * @param bool   $ignoreStrictCheck Whether to ignore the strict attribute check or not
1674     * @param int    $lineno            The template line where the attribute was called
1675     *
1676     * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true
1677     *
1678     * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false
1679     *
1680     * @internal
1681     */
1682    public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1)
1683    {
1684        $propertyNotAllowedError = null;
1685        if ($sandboxed && $item instanceof \Stringable) {
1686            $env->getExtension(SandboxExtension::class)->ensureToStringAllowed($item, $lineno, $source);
1687        }
1688
1689        // array
1690        if (Template::METHOD_CALL !== $type) {
1691            $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item;
1692
1693            if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) {
1694                try {
1695                    $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $arrayItem, $lineno, $source);
1696                } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) {
1697                    // The methodCheck path expects $item to be a string; stringify it here
1698                    // to avoid PHP 8.1+ implicit float-to-int deprecations on downstream
1699                    // array key lookups (e.g. isset($cache[$class][$item])).
1700                    $item = (string) $item;
1701                    goto methodCheck;
1702                }
1703            }
1704
1705            if (match (true) {
1706                \is_array($object) => \array_key_exists($arrayItem = (string) $arrayItem, $object),
1707                $object instanceof \ArrayAccess => $object->offsetExists($arrayItem),
1708                default => false,
1709            }) {
1710                if ($isDefinedTest) {
1711                    return true;
1712                }
1713
1714                return $object[$arrayItem];
1715            }
1716
1717            if (Template::ARRAY_CALL === $type || !\is_object($object)) {
1718                if ($isDefinedTest) {
1719                    return false;
1720                }
1721
1722                if ($ignoreStrictCheck || !$env->isStrictVariables()) {
1723                    return;
1724                }
1725
1726                if ($object instanceof \ArrayAccess) {
1727                    if (\is_object($arrayItem) || \is_array($arrayItem)) {
1728                        $message = \sprintf('Key of type "%s" does not exist in ArrayAccess-able object of class "%s".', get_debug_type($arrayItem), get_debug_type($object));
1729                    } else {
1730                        $message = \sprintf('Key "%s" does not exist in ArrayAccess-able object of class "%s".', $arrayItem, get_debug_type($object));
1731                    }
1732                } elseif (\is_object($object)) {
1733                    $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, get_debug_type($object));
1734                } elseif (\is_array($object)) {
1735                    if (!$object) {
1736                        $message = \sprintf('Key "%s" does not exist as the sequence/mapping is empty.', $arrayItem);
1737                    } else {
1738                        $message = \sprintf('Key "%s" for sequence/mapping with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object)));
1739                    }
1740                } elseif (Template::ARRAY_CALL === $type) {
1741                    if (null === $object) {
1742                        $message = \sprintf('Impossible to access a key ("%s") on a null variable.', $item);
1743                    } else {
1744                        $message = \sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object);
1745                    }
1746                } elseif (null === $object) {
1747                    $message = \sprintf('Impossible to access an attribute ("%s") on a null variable.', $item);
1748                } else {
1749                    $message = \sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object);
1750                }
1751
1752                throw new RuntimeError($message, $lineno, $source);
1753            }
1754        }
1755
1756        $item = (string) $item;
1757
1758        if (!\is_object($object)) {
1759            if ($isDefinedTest) {
1760                return false;
1761            }
1762
1763            if ($ignoreStrictCheck || !$env->isStrictVariables()) {
1764                return;
1765            }
1766
1767            if (null === $object) {
1768                $message = \sprintf('Impossible to invoke a method ("%s") on a null variable.', $item);
1769            } elseif (\is_array($object)) {
1770                $message = \sprintf('Impossible to invoke a method ("%s") on a sequence/mapping.', $item);
1771            } else {
1772                $message = \sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object);
1773            }
1774
1775            throw new RuntimeError($message, $lineno, $source);
1776        }
1777
1778        if ($object instanceof Template) {
1779            throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source);
1780        }
1781
1782        // object property
1783        if (Template::METHOD_CALL !== $type) {
1784            if ($sandboxed) {
1785                try {
1786                    $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source);
1787                } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) {
1788                    goto methodCheck;
1789                }
1790            }
1791
1792            static $propertyCheckers = [];
1793
1794            if (isset($object->$item)
1795                || ($propertyCheckers[$object::class][$item] ??= self::getPropertyChecker($object::class, $item))($object, $item)
1796            ) {
1797                if ($isDefinedTest) {
1798                    return true;
1799                }
1800
1801                return $object->$item;
1802            }
1803
1804            if ($object instanceof \DateTimeInterface && \in_array($item, ['date', 'timezone', 'timezone_type'], true)) {
1805                if ($isDefinedTest) {
1806                    return true;
1807                }
1808
1809                return ((array) $object)[$item];
1810            }
1811
1812            if (\defined($object::class.'::'.$item)) {
1813                if ($isDefinedTest) {
1814                    return true;
1815                }
1816
1817                return \constant($object::class.'::'.$item);
1818            }
1819        }
1820
1821        methodCheck:
1822
1823        static $cache = [];
1824
1825        $class = $object::class;
1826
1827        // object method
1828        // precedence: getXxx() > isXxx() > hasXxx()
1829        if (!isset($cache[$class])) {
1830            $methods = get_class_methods($object);
1831            if ($object instanceof \Closure) {
1832                $methods[] = '__invoke';
1833            }
1834            sort($methods);
1835            $lcMethods = array_map('strtolower', $methods);
1836            $classCache = [];
1837            foreach ($methods as $i => $method) {
1838                $classCache[$method] = $method;
1839                $classCache[$lcName = $lcMethods[$i]] = $method;
1840
1841                if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) {
1842                    $name = substr($method, 3);
1843                    $lcName = substr($lcName, 3);
1844                } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) {
1845                    $name = substr($method, 2);
1846                    $lcName = substr($lcName, 2);
1847                } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) {
1848                    $name = substr($method, 3);
1849                    $lcName = substr($lcName, 3);
1850                    if (\in_array('is'.$lcName, $lcMethods, true)) {
1851                        continue;
1852                    }
1853                } else {
1854                    continue;
1855                }
1856
1857                // skip get() and is() methods (in which case, $name is empty)
1858                if ($name) {
1859                    if (!isset($classCache[$name])) {
1860                        $classCache[$name] = $method;
1861                    }
1862
1863                    if (!isset($classCache[$lcName])) {
1864                        $classCache[$lcName] = $method;
1865                    }
1866                }
1867            }
1868            $cache[$class] = $classCache;
1869        }
1870
1871        $call = false;
1872        if (isset($cache[$class][$item])) {
1873            $method = $cache[$class][$item];
1874        } elseif (isset($cache[$class][$lcItem = strtolower($item)])) {
1875            $method = $cache[$class][$lcItem];
1876        } elseif (isset($cache[$class]['__call'])) {
1877            $method = $item;
1878            $call = true;
1879        } else {
1880            if ($isDefinedTest) {
1881                return false;
1882            }
1883
1884            if ($propertyNotAllowedError) {
1885                throw $propertyNotAllowedError;
1886            }
1887
1888            if ($ignoreStrictCheck || !$env->isStrictVariables()) {
1889                return;
1890            }
1891
1892            throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()", "is%1$s()", "has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source);
1893        }
1894
1895        if ($sandboxed) {
1896            try {
1897                $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source);
1898            } catch (SecurityNotAllowedMethodError $e) {
1899                if ($isDefinedTest) {
1900                    return false;
1901                }
1902
1903                if ($propertyNotAllowedError) {
1904                    throw $propertyNotAllowedError;
1905                }
1906
1907                throw $e;
1908            }
1909        }
1910
1911        if ($isDefinedTest) {
1912            return true;
1913        }
1914
1915        // Some objects throw exceptions when they have __call, and the method we try
1916        // to call is not supported. If ignoreStrictCheck is true, we should return null.
1917        try {
1918            $ret = $object->$method(...$arguments);
1919        } catch (\BadMethodCallException $e) {
1920            if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) {
1921                return;
1922            }
1923            throw $e;
1924        }
1925
1926        return $ret;
1927    }
1928
1929    /**
1930     * Returns the values from a single column in the input array.
1931     *
1932     * <pre>
1933     *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
1934     *
1935     *  {% set fruits = items|column('fruit') %}
1936     *
1937     *  {# fruits now contains ['apple', 'orange'] #}
1938     * </pre>
1939     *
1940     * @param array|\Traversable $array An array
1941     * @param int|string         $name  The column name
1942     * @param int|string|null    $index The column to use as the index/keys for the returned array
1943     *
1944     * @return array The array of values
1945     *
1946     * @internal
1947     */
1948    public static function column(Environment $env, bool $isSandboxed, $array, $name, $index = null): array
1949    {
1950        if (!is_iterable($array)) {
1951            throw new RuntimeError(\sprintf('The "column" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
1952        }
1953
1954        if ($array instanceof \Traversable) {
1955            $array = iterator_to_array($array);
1956        }
1957
1958        if ($isSandboxed) {
1959            // The sandbox might be enabled via a SourcePolicyInterface, in which case the SandboxExtension
1960            // would not consider the sandbox active without the current Source: $isSandboxed is already
1961            // computed against the call-site source, so check the policy directly to honor that decision.
1962            $policy = $env->getExtension(SandboxExtension::class)->getSecurityPolicy();
1963            foreach ($array as $item) {
1964                if (\is_object($item)) {
1965                    $policy->checkPropertyAllowed($item, (string) $name);
1966                    if (null !== $index) {
1967                        $policy->checkPropertyAllowed($item, (string) $index);
1968                    }
1969                }
1970            }
1971        }
1972
1973        return array_column($array, $name, $index);
1974    }
1975
1976    /**
1977     * @param \Closure $arrow
1978     *
1979     * @internal
1980     */
1981    public static function filter(Environment $env, bool $isSandboxed, $array, $arrow)
1982    {
1983        if (!is_iterable($array)) {
1984            throw new RuntimeError(\sprintf('The "filter" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array)));
1985        }
1986
1987        self::checkArrow($isSandboxed, $arrow, 'filter', 'filter');
1988
1989        if (\is_array($array)) {
1990            return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
1991        }
1992
1993        // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
1994        return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
1995    }
1996
1997    /**
1998     * @param \Closure $arrow
1999     *
2000     * @internal
2001     */
2002    public static function find(Environment $env, bool $isSandboxed, $array, $arrow)
2003    {
2004        if (!is_iterable($array)) {
2005            throw new RuntimeError(\sprintf('The "find" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
2006        }
2007
2008        self::checkArrow($isSandboxed, $arrow, 'find', 'filter');
2009
2010        foreach ($array as $k => $v) {
2011            if ($arrow($v, $k)) {
2012                return $v;
2013            }
2014        }
2015
2016        return null;
2017    }
2018
2019    /**
2020     * @param \Closure $arrow
2021     *
2022     * @internal
2023     */
2024    public static function map(Environment $env, bool $isSandboxed, $array, $arrow)
2025    {
2026        if (!is_iterable($array)) {
2027            throw new RuntimeError(\sprintf('The "map" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
2028        }
2029
2030        self::checkArrow($isSandboxed, $arrow, 'map', 'filter');
2031
2032        $r = [];
2033        foreach ($array as $k => $v) {
2034            $r[$k] = $arrow($v, $k);
2035        }
2036
2037        return $r;
2038    }
2039
2040    /**
2041     * @param \Closure $arrow
2042     *
2043     * @internal
2044     */
2045    public static function reduce(Environment $env, bool $isSandboxed, $array, $arrow, $initial = null)
2046    {
2047        if (!is_iterable($array)) {
2048            throw new RuntimeError(\sprintf('The "reduce" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
2049        }
2050
2051        self::checkArrow($isSandboxed, $arrow, 'reduce', 'filter');
2052
2053        $accumulator = $initial;
2054        foreach ($array as $key => $value) {
2055            $accumulator = $arrow($accumulator, $value, $key);
2056        }
2057
2058        return $accumulator;
2059    }
2060
2061    /**
2062     * @param \Closure $arrow
2063     *
2064     * @internal
2065     */
2066    public static function arraySome(Environment $env, $array, $arrow, bool $isSandboxed = false)
2067    {
2068        if (!is_iterable($array)) {
2069            throw new RuntimeError(\sprintf('The "has some" test expects a sequence or a mapping, got "%s".', get_debug_type($array)));
2070        }
2071
2072        self::checkArrow($isSandboxed, $arrow, 'has some', 'operator');
2073
2074        foreach ($array as $k => $v) {
2075            if ($arrow($v, $k)) {
2076                return true;
2077            }
2078        }
2079
2080        return false;
2081    }
2082
2083    /**
2084     * @param \Closure $arrow
2085     *
2086     * @internal
2087     */
2088    public static function arrayEvery(Environment $env, $array, $arrow, bool $isSandboxed = false)
2089    {
2090        if (!is_iterable($array)) {
2091            throw new RuntimeError(\sprintf('The "has every" test expects a sequence or a mapping, got "%s".', get_debug_type($array)));
2092        }
2093
2094        self::checkArrow($isSandboxed, $arrow, 'has every', 'operator');
2095
2096        foreach ($array as $k => $v) {
2097            if (!$arrow($v, $k)) {
2098                return false;
2099            }
2100        }
2101
2102        return true;
2103    }
2104
2105    /**
2106     * @internal
2107     */
2108    public static function checkArrow(bool $isSandboxed, $arrow, $thing, $type)
2109    {
2110        if ($arrow instanceof \Closure) {
2111            return;
2112        }
2113
2114        if ($isSandboxed) {
2115            throw new RuntimeError(\sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type));
2116        }
2117
2118        trigger_deprecation('twig/twig', '3.15', 'Passing a callable that is not a PHP \Closure as an argument to the "%s" %s is deprecated.', $thing, $type);
2119    }
2120
2121    /**
2122     * @internal to be removed in Twig 4
2123     */
2124    public static function captureOutput(iterable $body): string
2125    {
2126        $level = ob_get_level();
2127        ob_start();
2128
2129        try {
2130            foreach ($body as $data) {
2131                echo $data;
2132            }
2133        } catch (\Throwable $e) {
2134            while (ob_get_level() > $level) {
2135                ob_end_clean();
2136            }
2137
2138            throw $e;
2139        }
2140
2141        return ob_get_clean();
2142    }
2143
2144    /**
2145     * @internal
2146     */
2147    public static function parseParentFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
2148    {
2149        if (!$blockName = $parser->peekBlockStack()) {
2150            throw new SyntaxError('Calling the "parent" function outside of a block is forbidden.', $line, $parser->getStream()->getSourceContext());
2151        }
2152
2153        if (!$parser->hasInheritance()) {
2154            throw new SyntaxError('Calling the "parent" function on a template that does not call "extends" or "use" is forbidden.', $line, $parser->getStream()->getSourceContext());
2155        }
2156
2157        return new ParentExpression($blockName, $line);
2158    }
2159
2160    /**
2161     * @internal
2162     */
2163    public static function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
2164    {
2165        $fakeFunction = new TwigFunction('block', static fn ($name, $template = null) => null);
2166        $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args);
2167
2168        return new BlockReferenceExpression($args[0], $args[1] ?? null, $line);
2169    }
2170
2171    /**
2172     * @internal
2173     */
2174    public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
2175    {
2176        $fakeFunction = new TwigFunction('attribute', static fn ($variable, $attribute, $arguments = null) => null);
2177        $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args);
2178
2179        /*
2180        Deprecation to uncomment sometimes during the lifetime of the 4.x branch
2181        $src = $parser->getStream()->getSourceContext();
2182        $dep = new DeprecatedCallableInfo('twig/twig', '3.15', 'The "attribute" function is deprecated, use the "." notation instead.');
2183        $dep->setName('attribute');
2184        $dep->setType('function');
2185        $dep->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
2186        */
2187
2188        return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line);
2189    }
2190
2191    private static function getPropertyChecker(string $class, string $property): \Closure
2192    {
2193        static $classReflectors = [];
2194
2195        $class = $classReflectors[$class] ??= new \ReflectionClass($class);
2196
2197        if (!$class->hasProperty($property)) {
2198            static $propertyExists;
2199
2200            return $propertyExists ??= \Closure::fromCallable('property_exists');
2201        }
2202
2203        $property = $class->getProperty($property);
2204
2205        if (!$property->isPublic() || $property->isStatic()) {
2206            static $false;
2207
2208            return $false ??= static fn () => false;
2209        }
2210
2211        return static fn ($object) => $property->isInitialized($object);
2212    }
2213}
2214