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 {
13use Twig\ExpressionParser;
14use Twig\Node\Expression\Binary\AddBinary;
15use Twig\Node\Expression\Binary\AndBinary;
16use Twig\Node\Expression\Binary\BitwiseAndBinary;
17use Twig\Node\Expression\Binary\BitwiseOrBinary;
18use Twig\Node\Expression\Binary\BitwiseXorBinary;
19use Twig\Node\Expression\Binary\ConcatBinary;
20use Twig\Node\Expression\Binary\DivBinary;
21use Twig\Node\Expression\Binary\EndsWithBinary;
22use Twig\Node\Expression\Binary\EqualBinary;
23use Twig\Node\Expression\Binary\FloorDivBinary;
24use Twig\Node\Expression\Binary\GreaterBinary;
25use Twig\Node\Expression\Binary\GreaterEqualBinary;
26use Twig\Node\Expression\Binary\InBinary;
27use Twig\Node\Expression\Binary\LessBinary;
28use Twig\Node\Expression\Binary\LessEqualBinary;
29use Twig\Node\Expression\Binary\MatchesBinary;
30use Twig\Node\Expression\Binary\ModBinary;
31use Twig\Node\Expression\Binary\MulBinary;
32use Twig\Node\Expression\Binary\NotEqualBinary;
33use Twig\Node\Expression\Binary\NotInBinary;
34use Twig\Node\Expression\Binary\OrBinary;
35use Twig\Node\Expression\Binary\PowerBinary;
36use Twig\Node\Expression\Binary\RangeBinary;
37use Twig\Node\Expression\Binary\SpaceshipBinary;
38use Twig\Node\Expression\Binary\StartsWithBinary;
39use Twig\Node\Expression\Binary\SubBinary;
40use Twig\Node\Expression\Filter\DefaultFilter;
41use Twig\Node\Expression\NullCoalesceExpression;
42use Twig\Node\Expression\Test\ConstantTest;
43use Twig\Node\Expression\Test\DefinedTest;
44use Twig\Node\Expression\Test\DivisiblebyTest;
45use Twig\Node\Expression\Test\EvenTest;
46use Twig\Node\Expression\Test\NullTest;
47use Twig\Node\Expression\Test\OddTest;
48use Twig\Node\Expression\Test\SameasTest;
49use Twig\Node\Expression\Unary\NegUnary;
50use Twig\Node\Expression\Unary\NotUnary;
51use Twig\Node\Expression\Unary\PosUnary;
52use Twig\NodeVisitor\MacroAutoImportNodeVisitor;
53use Twig\TokenParser\ApplyTokenParser;
54use Twig\TokenParser\BlockTokenParser;
55use Twig\TokenParser\DeprecatedTokenParser;
56use Twig\TokenParser\DoTokenParser;
57use Twig\TokenParser\EmbedTokenParser;
58use Twig\TokenParser\ExtendsTokenParser;
59use Twig\TokenParser\FilterTokenParser;
60use Twig\TokenParser\FlushTokenParser;
61use Twig\TokenParser\ForTokenParser;
62use Twig\TokenParser\FromTokenParser;
63use Twig\TokenParser\IfTokenParser;
64use Twig\TokenParser\ImportTokenParser;
65use Twig\TokenParser\IncludeTokenParser;
66use Twig\TokenParser\MacroTokenParser;
67use Twig\TokenParser\SetTokenParser;
68use Twig\TokenParser\SpacelessTokenParser;
69use Twig\TokenParser\UseTokenParser;
70use Twig\TokenParser\WithTokenParser;
71use Twig\TwigFilter;
72use Twig\TwigFunction;
73use Twig\TwigTest;
74
75final class CoreExtension extends AbstractExtension
76{
77    private $dateFormats = ['F j, Y H:i', '%d days'];
78    private $numberFormat = [0, '.', ','];
79    private $timezone = null;
80    private $escapers = [];
81
82    /**
83     * Defines a new escaper to be used via the escape filter.
84     *
85     * @param string   $strategy The strategy name that should be used as a strategy in the escape call
86     * @param callable $callable A valid PHP callable
87     *
88     * @deprecated since Twig 2.11, to be removed in 3.0; use the same method on EscaperExtension instead
89     */
90    public function setEscaper($strategy, callable $callable)
91    {
92        @trigger_error(sprintf('The "%s" method is deprecated since Twig 2.11; use "%s::setEscaper" instead.', __METHOD__, EscaperExtension::class), \E_USER_DEPRECATED);
93
94        $this->escapers[$strategy] = $callable;
95    }
96
97    /**
98     * Gets all defined escapers.
99     *
100     * @return callable[] An array of escapers
101     *
102     * @deprecated since Twig 2.11, to be removed in 3.0; use the same method on EscaperExtension instead
103     */
104    public function getEscapers(/* $triggerDeprecation = true */)
105    {
106        if (0 === \func_num_args() || \func_get_arg(0)) {
107            @trigger_error(sprintf('The "%s" method is deprecated since Twig 2.11; use "%s::getEscapers" instead.', __METHOD__, EscaperExtension::class), \E_USER_DEPRECATED);
108        }
109
110        return $this->escapers;
111    }
112
113    /**
114     * Sets the default format to be used by the date filter.
115     *
116     * @param string $format             The default date format string
117     * @param string $dateIntervalFormat The default date interval format string
118     */
119    public function setDateFormat($format = null, $dateIntervalFormat = null)
120    {
121        if (null !== $format) {
122            $this->dateFormats[0] = $format;
123        }
124
125        if (null !== $dateIntervalFormat) {
126            $this->dateFormats[1] = $dateIntervalFormat;
127        }
128    }
129
130    /**
131     * Gets the default format to be used by the date filter.
132     *
133     * @return array The default date format string and the default date interval format string
134     */
135    public function getDateFormat()
136    {
137        return $this->dateFormats;
138    }
139
140    /**
141     * Sets the default timezone to be used by the date filter.
142     *
143     * @param \DateTimeZone|string $timezone The default timezone string or a \DateTimeZone object
144     */
145    public function setTimezone($timezone)
146    {
147        $this->timezone = $timezone instanceof \DateTimeZone ? $timezone : new \DateTimeZone($timezone);
148    }
149
150    /**
151     * Gets the default timezone to be used by the date filter.
152     *
153     * @return \DateTimeZone The default timezone currently in use
154     */
155    public function getTimezone()
156    {
157        if (null === $this->timezone) {
158            $this->timezone = new \DateTimeZone(date_default_timezone_get());
159        }
160
161        return $this->timezone;
162    }
163
164    /**
165     * Sets the default format to be used by the number_format filter.
166     *
167     * @param int    $decimal      the number of decimal places to use
168     * @param string $decimalPoint the character(s) to use for the decimal point
169     * @param string $thousandSep  the character(s) to use for the thousands separator
170     */
171    public function setNumberFormat($decimal, $decimalPoint, $thousandSep)
172    {
173        $this->numberFormat = [$decimal, $decimalPoint, $thousandSep];
174    }
175
176    /**
177     * Get the default format used by the number_format filter.
178     *
179     * @return array The arguments for number_format()
180     */
181    public function getNumberFormat()
182    {
183        return $this->numberFormat;
184    }
185
186    public function getTokenParsers()
187    {
188        return [
189            new ApplyTokenParser(),
190            new ForTokenParser(),
191            new IfTokenParser(),
192            new ExtendsTokenParser(),
193            new IncludeTokenParser(),
194            new BlockTokenParser(),
195            new UseTokenParser(),
196            new FilterTokenParser(),
197            new MacroTokenParser(),
198            new ImportTokenParser(),
199            new FromTokenParser(),
200            new SetTokenParser(),
201            new SpacelessTokenParser(),
202            new FlushTokenParser(),
203            new DoTokenParser(),
204            new EmbedTokenParser(),
205            new WithTokenParser(),
206            new DeprecatedTokenParser(),
207        ];
208    }
209
210    public function getFilters()
211    {
212        return [
213            // formatting filters
214            new TwigFilter('date', 'twig_date_format_filter', ['needs_environment' => true]),
215            new TwigFilter('date_modify', 'twig_date_modify_filter', ['needs_environment' => true]),
216            new TwigFilter('format', 'sprintf'),
217            new TwigFilter('replace', 'twig_replace_filter'),
218            new TwigFilter('number_format', 'twig_number_format_filter', ['needs_environment' => true]),
219            new TwigFilter('abs', 'abs'),
220            new TwigFilter('round', 'twig_round'),
221
222            // encoding
223            new TwigFilter('url_encode', 'twig_urlencode_filter'),
224            new TwigFilter('json_encode', 'json_encode'),
225            new TwigFilter('convert_encoding', 'twig_convert_encoding'),
226
227            // string filters
228            new TwigFilter('title', 'twig_title_string_filter', ['needs_environment' => true]),
229            new TwigFilter('capitalize', 'twig_capitalize_string_filter', ['needs_environment' => true]),
230            new TwigFilter('upper', 'twig_upper_filter', ['needs_environment' => true]),
231            new TwigFilter('lower', 'twig_lower_filter', ['needs_environment' => true]),
232            new TwigFilter('striptags', 'strip_tags'),
233            new TwigFilter('trim', 'twig_trim_filter'),
234            new TwigFilter('nl2br', 'nl2br', ['pre_escape' => 'html', 'is_safe' => ['html']]),
235            new TwigFilter('spaceless', 'twig_spaceless', ['is_safe' => ['html']]),
236
237            // array helpers
238            new TwigFilter('join', 'twig_join_filter'),
239            new TwigFilter('split', 'twig_split_filter', ['needs_environment' => true]),
240            new TwigFilter('sort', 'twig_sort_filter'),
241            new TwigFilter('merge', 'twig_array_merge'),
242            new TwigFilter('batch', 'twig_array_batch'),
243            new TwigFilter('column', 'twig_array_column'),
244            new TwigFilter('filter', 'twig_array_filter', ['needs_environment' => true]),
245            new TwigFilter('map', 'twig_array_map', ['needs_environment' => true]),
246            new TwigFilter('reduce', 'twig_array_reduce', ['needs_environment' => true]),
247
248            // string/array filters
249            new TwigFilter('reverse', 'twig_reverse_filter', ['needs_environment' => true]),
250            new TwigFilter('length', 'twig_length_filter', ['needs_environment' => true]),
251            new TwigFilter('slice', 'twig_slice', ['needs_environment' => true]),
252            new TwigFilter('first', 'twig_first', ['needs_environment' => true]),
253            new TwigFilter('last', 'twig_last', ['needs_environment' => true]),
254
255            // iteration and runtime
256            new TwigFilter('default', '_twig_default_filter', ['node_class' => DefaultFilter::class]),
257            new TwigFilter('keys', 'twig_get_array_keys_filter'),
258        ];
259    }
260
261    public function getFunctions()
262    {
263        return [
264            new TwigFunction('max', 'max'),
265            new TwigFunction('min', 'min'),
266            new TwigFunction('range', 'range'),
267            new TwigFunction('constant', 'twig_constant'),
268            new TwigFunction('cycle', 'twig_cycle'),
269            new TwigFunction('random', 'twig_random', ['needs_environment' => true]),
270            new TwigFunction('date', 'twig_date_converter', ['needs_environment' => true]),
271            new TwigFunction('include', 'twig_include', ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]),
272            new TwigFunction('source', 'twig_source', ['needs_environment' => true, 'is_safe' => ['all']]),
273        ];
274    }
275
276    public function getTests()
277    {
278        return [
279            new TwigTest('even', null, ['node_class' => EvenTest::class]),
280            new TwigTest('odd', null, ['node_class' => OddTest::class]),
281            new TwigTest('defined', null, ['node_class' => DefinedTest::class]),
282            new TwigTest('same as', null, ['node_class' => SameasTest::class, 'one_mandatory_argument' => true]),
283            new TwigTest('none', null, ['node_class' => NullTest::class]),
284            new TwigTest('null', null, ['node_class' => NullTest::class]),
285            new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]),
286            new TwigTest('constant', null, ['node_class' => ConstantTest::class]),
287            new TwigTest('empty', 'twig_test_empty'),
288            new TwigTest('iterable', 'twig_test_iterable'),
289        ];
290    }
291
292    public function getNodeVisitors()
293    {
294        return [new MacroAutoImportNodeVisitor()];
295    }
296
297    public function getOperators()
298    {
299        return [
300            [
301                'not' => ['precedence' => 50, 'class' => NotUnary::class],
302                '-' => ['precedence' => 500, 'class' => NegUnary::class],
303                '+' => ['precedence' => 500, 'class' => PosUnary::class],
304            ],
305            [
306                'or' => ['precedence' => 10, 'class' => OrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
307                'and' => ['precedence' => 15, 'class' => AndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
308                'b-or' => ['precedence' => 16, 'class' => BitwiseOrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
309                'b-xor' => ['precedence' => 17, 'class' => BitwiseXorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
310                'b-and' => ['precedence' => 18, 'class' => BitwiseAndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
311                '==' => ['precedence' => 20, 'class' => EqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
312                '!=' => ['precedence' => 20, 'class' => NotEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
313                '<=>' => ['precedence' => 20, 'class' => SpaceshipBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
314                '<' => ['precedence' => 20, 'class' => LessBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
315                '>' => ['precedence' => 20, 'class' => GreaterBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
316                '>=' => ['precedence' => 20, 'class' => GreaterEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
317                '<=' => ['precedence' => 20, 'class' => LessEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
318                'not in' => ['precedence' => 20, 'class' => NotInBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
319                'in' => ['precedence' => 20, 'class' => InBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
320                'matches' => ['precedence' => 20, 'class' => MatchesBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
321                'starts with' => ['precedence' => 20, 'class' => StartsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
322                'ends with' => ['precedence' => 20, 'class' => EndsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
323                '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
324                '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
325                '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
326                '~' => ['precedence' => 40, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
327                '*' => ['precedence' => 60, 'class' => MulBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
328                '/' => ['precedence' => 60, 'class' => DivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
329                '//' => ['precedence' => 60, 'class' => FloorDivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
330                '%' => ['precedence' => 60, 'class' => ModBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
331                'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
332                'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
333                '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
334                '??' => ['precedence' => 300, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
335            ],
336        ];
337    }
338}
339
340class_alias('Twig\Extension\CoreExtension', 'Twig_Extension_Core');
341}
342
343namespace {
344    use Twig\Environment;
345    use Twig\Error\LoaderError;
346    use Twig\Error\RuntimeError;
347    use Twig\Extension\CoreExtension;
348    use Twig\Extension\SandboxExtension;
349    use Twig\Markup;
350    use Twig\Source;
351    use Twig\Template;
352    use Twig\TemplateWrapper;
353
354/**
355 * Cycles over a value.
356 *
357 * @param \ArrayAccess|array $values
358 * @param int                $position The cycle position
359 *
360 * @return string The next value in the cycle
361 */
362function twig_cycle($values, $position)
363{
364    if (!\is_array($values) && !$values instanceof \ArrayAccess) {
365        return $values;
366    }
367
368    return $values[$position % \count($values)];
369}
370
371/**
372 * Returns a random value depending on the supplied parameter type:
373 * - a random item from a \Traversable or array
374 * - a random character from a string
375 * - a random integer between 0 and the integer parameter.
376 *
377 * @param \Traversable|array|int|float|string $values The values to pick a random item from
378 * @param int|null                            $max    Maximum value used when $values is an int
379 *
380 * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is)
381 *
382 * @return mixed A random value from the given sequence
383 */
384function twig_random(Environment $env, $values = null, $max = null)
385{
386    if (null === $values) {
387        return null === $max ? mt_rand() : mt_rand(0, $max);
388    }
389
390    if (\is_int($values) || \is_float($values)) {
391        if (null === $max) {
392            if ($values < 0) {
393                $max = 0;
394                $min = $values;
395            } else {
396                $max = $values;
397                $min = 0;
398            }
399        } else {
400            $min = $values;
401            $max = $max;
402        }
403
404        return mt_rand($min, $max);
405    }
406
407    if (\is_string($values)) {
408        if ('' === $values) {
409            return '';
410        }
411
412        $charset = $env->getCharset();
413
414        if ('UTF-8' !== $charset) {
415            $values = twig_convert_encoding($values, 'UTF-8', $charset);
416        }
417
418        // unicode version of str_split()
419        // split at all positions, but not after the start and not before the end
420        $values = preg_split('/(?<!^)(?!$)/u', $values);
421
422        if ('UTF-8' !== $charset) {
423            foreach ($values as $i => $value) {
424                $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8');
425            }
426        }
427    }
428
429    if (!twig_test_iterable($values)) {
430        return $values;
431    }
432
433    $values = twig_to_array($values);
434
435    if (0 === \count($values)) {
436        throw new RuntimeError('The random function cannot pick from an empty array.');
437    }
438
439    return $values[array_rand($values, 1)];
440}
441
442/**
443 * Converts a date to the given format.
444 *
445 *   {{ post.published_at|date("m/d/Y") }}
446 *
447 * @param \DateTimeInterface|\DateInterval|string $date     A date
448 * @param string|null                             $format   The target format, null to use the default
449 * @param \DateTimeZone|string|false|null         $timezone The target timezone, null to use the default, false to leave unchanged
450 *
451 * @return string The formatted date
452 */
453function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null)
454{
455    if (null === $format) {
456        $formats = $env->getExtension(CoreExtension::class)->getDateFormat();
457        $format = $date instanceof \DateInterval ? $formats[1] : $formats[0];
458    }
459
460    if ($date instanceof \DateInterval) {
461        return $date->format($format);
462    }
463
464    return twig_date_converter($env, $date, $timezone)->format($format);
465}
466
467/**
468 * Returns a new date object modified.
469 *
470 *   {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
471 *
472 * @param \DateTimeInterface|string $date     A date
473 * @param string                    $modifier A modifier string
474 *
475 * @return \DateTimeInterface
476 */
477function twig_date_modify_filter(Environment $env, $date, $modifier)
478{
479    $date = twig_date_converter($env, $date, false);
480
481    return $date->modify($modifier);
482}
483
484/**
485 * Converts an input to a \DateTime instance.
486 *
487 *    {% if date(user.created_at) < date('+2days') %}
488 *      {# do something #}
489 *    {% endif %}
490 *
491 * @param \DateTimeInterface|string|null  $date     A date or null to use the current time
492 * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
493 *
494 * @return \DateTimeInterface
495 */
496function twig_date_converter(Environment $env, $date = null, $timezone = null)
497{
498    // determine the timezone
499    if (false !== $timezone) {
500        if (null === $timezone) {
501            $timezone = $env->getExtension(CoreExtension::class)->getTimezone();
502        } elseif (!$timezone instanceof \DateTimeZone) {
503            $timezone = new \DateTimeZone($timezone);
504        }
505    }
506
507    // immutable dates
508    if ($date instanceof \DateTimeImmutable) {
509        return false !== $timezone ? $date->setTimezone($timezone) : $date;
510    }
511
512    if ($date instanceof \DateTimeInterface) {
513        $date = clone $date;
514        if (false !== $timezone) {
515            $date->setTimezone($timezone);
516        }
517
518        return $date;
519    }
520
521    if (null === $date || 'now' === $date) {
522        if (null === $date) {
523            $date = 'now';
524        }
525
526        return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone());
527    }
528
529    $asString = (string) $date;
530    if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
531        $date = new \DateTime('@'.$date);
532    } else {
533        $date = new \DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone());
534    }
535
536    if (false !== $timezone) {
537        $date->setTimezone($timezone);
538    }
539
540    return $date;
541}
542
543/**
544 * Replaces strings within a string.
545 *
546 * @param string             $str  String to replace in
547 * @param array|\Traversable $from Replace values
548 *
549 * @return string
550 */
551function twig_replace_filter($str, $from)
552{
553    if (!twig_test_iterable($from)) {
554        throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from)));
555    }
556
557    return strtr($str, twig_to_array($from));
558}
559
560/**
561 * Rounds a number.
562 *
563 * @param int|float $value     The value to round
564 * @param int|float $precision The rounding precision
565 * @param string    $method    The method to use for rounding
566 *
567 * @return int|float The rounded number
568 */
569function twig_round($value, $precision = 0, $method = 'common')
570{
571    if ('common' === $method) {
572        return round($value, $precision);
573    }
574
575    if ('ceil' !== $method && 'floor' !== $method) {
576        throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.');
577    }
578
579    return $method($value * 10 ** $precision) / 10 ** $precision;
580}
581
582/**
583 * Number format filter.
584 *
585 * All of the formatting options can be left null, in that case the defaults will
586 * be used.  Supplying any of the parameters will override the defaults set in the
587 * environment object.
588 *
589 * @param mixed  $number       A float/int/string of the number to format
590 * @param int    $decimal      the number of decimal points to display
591 * @param string $decimalPoint the character(s) to use for the decimal point
592 * @param string $thousandSep  the character(s) to use for the thousands separator
593 *
594 * @return string The formatted number
595 */
596function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null)
597{
598    $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat();
599    if (null === $decimal) {
600        $decimal = $defaults[0];
601    }
602
603    if (null === $decimalPoint) {
604        $decimalPoint = $defaults[1];
605    }
606
607    if (null === $thousandSep) {
608        $thousandSep = $defaults[2];
609    }
610
611    return number_format((float) $number, $decimal, $decimalPoint, $thousandSep);
612}
613
614/**
615 * URL encodes (RFC 3986) a string as a path segment or an array as a query string.
616 *
617 * @param string|array $url A URL or an array of query parameters
618 *
619 * @return string The URL encoded value
620 */
621function twig_urlencode_filter($url)
622{
623    if (\is_array($url)) {
624        return http_build_query($url, '', '&', \PHP_QUERY_RFC3986);
625    }
626
627    return rawurlencode($url);
628}
629
630/**
631 * Merges an array with another one.
632 *
633 *  {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
634 *
635 *  {% set items = items|merge({ 'peugeot': 'car' }) %}
636 *
637 *  {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #}
638 *
639 * @param array|\Traversable $arr1 An array
640 * @param array|\Traversable $arr2 An array
641 *
642 * @return array The merged array
643 */
644function twig_array_merge($arr1, $arr2)
645{
646    if (!twig_test_iterable($arr1)) {
647        throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($arr1)));
648    }
649
650    if (!twig_test_iterable($arr2)) {
651        throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', \gettype($arr2)));
652    }
653
654    return array_merge(twig_to_array($arr1), twig_to_array($arr2));
655}
656
657/**
658 * Slices a variable.
659 *
660 * @param mixed $item         A variable
661 * @param int   $start        Start of the slice
662 * @param int   $length       Size of the slice
663 * @param bool  $preserveKeys Whether to preserve key or not (when the input is an array)
664 *
665 * @return mixed The sliced variable
666 */
667function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false)
668{
669    if ($item instanceof \Traversable) {
670        while ($item instanceof \IteratorAggregate) {
671            $item = $item->getIterator();
672        }
673
674        if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) {
675            try {
676                return iterator_to_array(new \LimitIterator($item, $start, null === $length ? -1 : $length), $preserveKeys);
677            } catch (\OutOfBoundsException $e) {
678                return [];
679            }
680        }
681
682        $item = iterator_to_array($item, $preserveKeys);
683    }
684
685    if (\is_array($item)) {
686        return \array_slice($item, $start, $length, $preserveKeys);
687    }
688
689    $item = (string) $item;
690
691    return (string) mb_substr($item, $start, $length, $env->getCharset());
692}
693
694/**
695 * Returns the first element of the item.
696 *
697 * @param mixed $item A variable
698 *
699 * @return mixed The first element of the item
700 */
701function twig_first(Environment $env, $item)
702{
703    $elements = twig_slice($env, $item, 0, 1, false);
704
705    return \is_string($elements) ? $elements : current($elements);
706}
707
708/**
709 * Returns the last element of the item.
710 *
711 * @param mixed $item A variable
712 *
713 * @return mixed The last element of the item
714 */
715function twig_last(Environment $env, $item)
716{
717    $elements = twig_slice($env, $item, -1, 1, false);
718
719    return \is_string($elements) ? $elements : current($elements);
720}
721
722/**
723 * Joins the values to a string.
724 *
725 * The separators between elements are empty strings per default, you can define them with the optional parameters.
726 *
727 *  {{ [1, 2, 3]|join(', ', ' and ') }}
728 *  {# returns 1, 2 and 3 #}
729 *
730 *  {{ [1, 2, 3]|join('|') }}
731 *  {# returns 1|2|3 #}
732 *
733 *  {{ [1, 2, 3]|join }}
734 *  {# returns 123 #}
735 *
736 * @param array       $value An array
737 * @param string      $glue  The separator
738 * @param string|null $and   The separator for the last pair
739 *
740 * @return string The concatenated string
741 */
742function twig_join_filter($value, $glue = '', $and = null)
743{
744    if (!twig_test_iterable($value)) {
745        $value = (array) $value;
746    }
747
748    $value = twig_to_array($value, false);
749
750    if (0 === \count($value)) {
751        return '';
752    }
753
754    if (null === $and || $and === $glue) {
755        return implode($glue, $value);
756    }
757
758    if (1 === \count($value)) {
759        return $value[0];
760    }
761
762    return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1];
763}
764
765/**
766 * Splits the string into an array.
767 *
768 *  {{ "one,two,three"|split(',') }}
769 *  {# returns [one, two, three] #}
770 *
771 *  {{ "one,two,three,four,five"|split(',', 3) }}
772 *  {# returns [one, two, "three,four,five"] #}
773 *
774 *  {{ "123"|split('') }}
775 *  {# returns [1, 2, 3] #}
776 *
777 *  {{ "aabbcc"|split('', 2) }}
778 *  {# returns [aa, bb, cc] #}
779 *
780 * @param string $value     A string
781 * @param string $delimiter The delimiter
782 * @param int    $limit     The limit
783 *
784 * @return array The split string as an array
785 */
786function twig_split_filter(Environment $env, $value, $delimiter, $limit = null)
787{
788    if (\strlen($delimiter) > 0) {
789        return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
790    }
791
792    if ($limit <= 1) {
793        return preg_split('/(?<!^)(?!$)/u', $value);
794    }
795
796    $length = mb_strlen($value, $env->getCharset());
797    if ($length < $limit) {
798        return [$value];
799    }
800
801    $r = [];
802    for ($i = 0; $i < $length; $i += $limit) {
803        $r[] = mb_substr($value, $i, $limit, $env->getCharset());
804    }
805
806    return $r;
807}
808
809// The '_default' filter is used internally to avoid using the ternary operator
810// which costs a lot for big contexts (before PHP 5.4). So, on average,
811// a function call is cheaper.
812/**
813 * @internal
814 */
815function _twig_default_filter($value, $default = '')
816{
817    if (twig_test_empty($value)) {
818        return $default;
819    }
820
821    return $value;
822}
823
824/**
825 * Returns the keys for the given array.
826 *
827 * It is useful when you want to iterate over the keys of an array:
828 *
829 *  {% for key in array|keys %}
830 *      {# ... #}
831 *  {% endfor %}
832 *
833 * @param array $array An array
834 *
835 * @return array The keys
836 */
837function twig_get_array_keys_filter($array)
838{
839    if ($array instanceof \Traversable) {
840        while ($array instanceof \IteratorAggregate) {
841            $array = $array->getIterator();
842        }
843
844        if ($array instanceof \Iterator) {
845            $keys = [];
846            $array->rewind();
847            while ($array->valid()) {
848                $keys[] = $array->key();
849                $array->next();
850            }
851
852            return $keys;
853        }
854
855        $keys = [];
856        foreach ($array as $key => $item) {
857            $keys[] = $key;
858        }
859
860        return $keys;
861    }
862
863    if (!\is_array($array)) {
864        return [];
865    }
866
867    return array_keys($array);
868}
869
870/**
871 * Reverses a variable.
872 *
873 * @param array|\Traversable|string $item         An array, a \Traversable instance, or a string
874 * @param bool                      $preserveKeys Whether to preserve key or not
875 *
876 * @return mixed The reversed input
877 */
878function twig_reverse_filter(Environment $env, $item, $preserveKeys = false)
879{
880    if ($item instanceof \Traversable) {
881        return array_reverse(iterator_to_array($item), $preserveKeys);
882    }
883
884    if (\is_array($item)) {
885        return array_reverse($item, $preserveKeys);
886    }
887
888    $string = (string) $item;
889
890    $charset = $env->getCharset();
891
892    if ('UTF-8' !== $charset) {
893        $item = twig_convert_encoding($string, 'UTF-8', $charset);
894    }
895
896    preg_match_all('/./us', $item, $matches);
897
898    $string = implode('', array_reverse($matches[0]));
899
900    if ('UTF-8' !== $charset) {
901        $string = twig_convert_encoding($string, $charset, 'UTF-8');
902    }
903
904    return $string;
905}
906
907/**
908 * Sorts an array.
909 *
910 * @param array|\Traversable $array
911 *
912 * @return array
913 */
914function twig_sort_filter($array, $arrow = null)
915{
916    if ($array instanceof \Traversable) {
917        $array = iterator_to_array($array);
918    } elseif (!\is_array($array)) {
919        throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
920    }
921
922    if (null !== $arrow) {
923        uasort($array, $arrow);
924    } else {
925        asort($array);
926    }
927
928    return $array;
929}
930
931/**
932 * @internal
933 */
934function twig_in_filter($value, $compare)
935{
936    if ($value instanceof Markup) {
937        $value = (string) $value;
938    }
939    if ($compare instanceof Markup) {
940        $compare = (string) $compare;
941    }
942
943    if (\is_array($compare)) {
944        return \in_array($value, $compare, \is_object($value) || \is_resource($value));
945    } elseif (\is_string($compare) && (\is_string($value) || \is_int($value) || \is_float($value))) {
946        return '' === $value || false !== strpos($compare, (string) $value);
947    } elseif ($compare instanceof \Traversable) {
948        if (\is_object($value) || \is_resource($value)) {
949            foreach ($compare as $item) {
950                if ($item === $value) {
951                    return true;
952                }
953            }
954        } else {
955            foreach ($compare as $item) {
956                if ($item == $value) {
957                    return true;
958                }
959            }
960        }
961
962        return false;
963    }
964
965    return false;
966}
967
968/**
969 * Returns a trimmed string.
970 *
971 * @return string
972 *
973 * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both')
974 */
975function twig_trim_filter($string, $characterMask = null, $side = 'both')
976{
977    if (null === $characterMask) {
978        $characterMask = " \t\n\r\0\x0B";
979    }
980
981    switch ($side) {
982        case 'both':
983            return trim($string, $characterMask);
984        case 'left':
985            return ltrim($string, $characterMask);
986        case 'right':
987            return rtrim($string, $characterMask);
988        default:
989            throw new RuntimeError('Trimming side must be "left", "right" or "both".');
990    }
991}
992
993/**
994 * Removes whitespaces between HTML tags.
995 *
996 * @return string
997 */
998function twig_spaceless($content)
999{
1000    return trim(preg_replace('/>\s+</', '><', $content));
1001}
1002
1003function twig_convert_encoding($string, $to, $from)
1004{
1005    if (!\function_exists('iconv')) {
1006        throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.');
1007    }
1008
1009    return iconv($from, $to, $string);
1010}
1011
1012/**
1013 * Returns the length of a variable.
1014 *
1015 * @param mixed $thing A variable
1016 *
1017 * @return int The length of the value
1018 */
1019function twig_length_filter(Environment $env, $thing)
1020{
1021    if (null === $thing) {
1022        return 0;
1023    }
1024
1025    if (is_scalar($thing)) {
1026        return mb_strlen($thing, $env->getCharset());
1027    }
1028
1029    if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) {
1030        return \count($thing);
1031    }
1032
1033    if ($thing instanceof \Traversable) {
1034        return iterator_count($thing);
1035    }
1036
1037    if (method_exists($thing, '__toString') && !$thing instanceof \Countable) {
1038        return mb_strlen((string) $thing, $env->getCharset());
1039    }
1040
1041    return 1;
1042}
1043
1044/**
1045 * Converts a string to uppercase.
1046 *
1047 * @param string $string A string
1048 *
1049 * @return string The uppercased string
1050 */
1051function twig_upper_filter(Environment $env, $string)
1052{
1053    return mb_strtoupper($string, $env->getCharset());
1054}
1055
1056/**
1057 * Converts a string to lowercase.
1058 *
1059 * @param string $string A string
1060 *
1061 * @return string The lowercased string
1062 */
1063function twig_lower_filter(Environment $env, $string)
1064{
1065    return mb_strtolower($string, $env->getCharset());
1066}
1067
1068/**
1069 * Returns a titlecased string.
1070 *
1071 * @param string $string A string
1072 *
1073 * @return string The titlecased string
1074 */
1075function twig_title_string_filter(Environment $env, $string)
1076{
1077    if (null !== $charset = $env->getCharset()) {
1078        return mb_convert_case($string, \MB_CASE_TITLE, $charset);
1079    }
1080
1081    return ucwords(strtolower($string));
1082}
1083
1084/**
1085 * Returns a capitalized string.
1086 *
1087 * @param string $string A string
1088 *
1089 * @return string The capitalized string
1090 */
1091function twig_capitalize_string_filter(Environment $env, $string)
1092{
1093    $charset = $env->getCharset();
1094
1095    return mb_strtoupper(mb_substr($string, 0, 1, $charset), $charset).mb_strtolower(mb_substr($string, 1, null, $charset), $charset);
1096}
1097
1098/**
1099 * @internal
1100 */
1101function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source)
1102{
1103    if (!method_exists($template, $method)) {
1104        $parent = $template;
1105        while ($parent = $parent->getParent($context)) {
1106            if (method_exists($parent, $method)) {
1107                return $parent->$method(...$args);
1108            }
1109        }
1110
1111        throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source);
1112    }
1113
1114    return $template->$method(...$args);
1115}
1116
1117/**
1118 * @internal
1119 */
1120function twig_ensure_traversable($seq)
1121{
1122    if ($seq instanceof \Traversable || \is_array($seq)) {
1123        return $seq;
1124    }
1125
1126    return [];
1127}
1128
1129/**
1130 * @internal
1131 */
1132function twig_to_array($seq, $preserveKeys = true)
1133{
1134    if ($seq instanceof \Traversable) {
1135        return iterator_to_array($seq, $preserveKeys);
1136    }
1137
1138    if (!\is_array($seq)) {
1139        return $seq;
1140    }
1141
1142    return $preserveKeys ? $seq : array_values($seq);
1143}
1144
1145/**
1146 * Checks if a variable is empty.
1147 *
1148 *    {# evaluates to true if the foo variable is null, false, or the empty string #}
1149 *    {% if foo is empty %}
1150 *        {# ... #}
1151 *    {% endif %}
1152 *
1153 * @param mixed $value A variable
1154 *
1155 * @return bool true if the value is empty, false otherwise
1156 */
1157function twig_test_empty($value)
1158{
1159    if ($value instanceof \Countable) {
1160        return 0 === \count($value);
1161    }
1162
1163    if ($value instanceof \Traversable) {
1164        return !iterator_count($value);
1165    }
1166
1167    if (\is_object($value) && method_exists($value, '__toString')) {
1168        return '' === (string) $value;
1169    }
1170
1171    return '' === $value || false === $value || null === $value || [] === $value;
1172}
1173
1174/**
1175 * Checks if a variable is traversable.
1176 *
1177 *    {# evaluates to true if the foo variable is an array or a traversable object #}
1178 *    {% if foo is iterable %}
1179 *        {# ... #}
1180 *    {% endif %}
1181 *
1182 * @param mixed $value A variable
1183 *
1184 * @return bool true if the value is traversable
1185 */
1186function twig_test_iterable($value)
1187{
1188    return $value instanceof \Traversable || \is_array($value);
1189}
1190
1191/**
1192 * Renders a template.
1193 *
1194 * @param array        $context
1195 * @param string|array $template      The template to render or an array of templates to try consecutively
1196 * @param array        $variables     The variables to pass to the template
1197 * @param bool         $withContext
1198 * @param bool         $ignoreMissing Whether to ignore missing templates or not
1199 * @param bool         $sandboxed     Whether to sandbox the template or not
1200 *
1201 * @return string The rendered template
1202 */
1203function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false)
1204{
1205    $alreadySandboxed = false;
1206    $sandbox = null;
1207    if ($withContext) {
1208        $variables = array_merge($context, $variables);
1209    }
1210
1211    if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) {
1212        $sandbox = $env->getExtension(SandboxExtension::class);
1213        if (!$alreadySandboxed = $sandbox->isSandboxed()) {
1214            $sandbox->enableSandbox();
1215        }
1216
1217        foreach ((\is_array($template) ? $template : [$template]) as $name) {
1218            // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security
1219            if ($name instanceof TemplateWrapper || $name instanceof Template) {
1220                $name->unwrap()->checkSecurity();
1221            }
1222        }
1223    }
1224
1225    try {
1226        $loaded = null;
1227        try {
1228            $loaded = $env->resolveTemplate($template);
1229        } catch (LoaderError $e) {
1230            if (!$ignoreMissing) {
1231                throw $e;
1232            }
1233        }
1234
1235        return $loaded ? $loaded->render($variables) : '';
1236    } finally {
1237        if ($isSandboxed && !$alreadySandboxed) {
1238            $sandbox->disableSandbox();
1239        }
1240    }
1241}
1242
1243/**
1244 * Returns a template content without rendering it.
1245 *
1246 * @param string $name          The template name
1247 * @param bool   $ignoreMissing Whether to ignore missing templates or not
1248 *
1249 * @return string The template source
1250 */
1251function twig_source(Environment $env, $name, $ignoreMissing = false)
1252{
1253    $loader = $env->getLoader();
1254    try {
1255        return $loader->getSourceContext($name)->getCode();
1256    } catch (LoaderError $e) {
1257        if (!$ignoreMissing) {
1258            throw $e;
1259        }
1260    }
1261}
1262
1263/**
1264 * Provides the ability to get constants from instances as well as class/global constants.
1265 *
1266 * @param string      $constant The name of the constant
1267 * @param object|null $object   The object to get the constant from
1268 *
1269 * @return string
1270 */
1271function twig_constant($constant, $object = null)
1272{
1273    if (null !== $object) {
1274        $constant = \get_class($object).'::'.$constant;
1275    }
1276
1277    return \constant($constant);
1278}
1279
1280/**
1281 * Checks if a constant exists.
1282 *
1283 * @param string      $constant The name of the constant
1284 * @param object|null $object   The object to get the constant from
1285 *
1286 * @return bool
1287 */
1288function twig_constant_is_defined($constant, $object = null)
1289{
1290    if (null !== $object) {
1291        $constant = \get_class($object).'::'.$constant;
1292    }
1293
1294    return \defined($constant);
1295}
1296
1297/**
1298 * Batches item.
1299 *
1300 * @param array $items An array of items
1301 * @param int   $size  The size of the batch
1302 * @param mixed $fill  A value used to fill missing items
1303 *
1304 * @return array
1305 */
1306function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
1307{
1308    if (!twig_test_iterable($items)) {
1309        throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items)));
1310    }
1311
1312    $size = ceil($size);
1313
1314    $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys);
1315
1316    if (null !== $fill && $result) {
1317        $last = \count($result) - 1;
1318        if ($fillCount = $size - \count($result[$last])) {
1319            for ($i = 0; $i < $fillCount; ++$i) {
1320                $result[$last][] = $fill;
1321            }
1322        }
1323    }
1324
1325    return $result;
1326}
1327
1328/**
1329 * Returns the attribute value for a given array/object.
1330 *
1331 * @param mixed  $object            The object or array from where to get the item
1332 * @param mixed  $item              The item to get from the array or object
1333 * @param array  $arguments         An array of arguments to pass if the item is an object method
1334 * @param string $type              The type of attribute (@see \Twig\Template constants)
1335 * @param bool   $isDefinedTest     Whether this is only a defined check
1336 * @param bool   $ignoreStrictCheck Whether to ignore the strict attribute check or not
1337 * @param int    $lineno            The template line where the attribute was called
1338 *
1339 * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true
1340 *
1341 * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false
1342 *
1343 * @internal
1344 */
1345function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1)
1346{
1347    // array
1348    if (/* Template::METHOD_CALL */ 'method' !== $type) {
1349        $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item;
1350
1351        if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object)))
1352            || ($object instanceof ArrayAccess && isset($object[$arrayItem]))
1353        ) {
1354            if ($isDefinedTest) {
1355                return true;
1356            }
1357
1358            return $object[$arrayItem];
1359        }
1360
1361        if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) {
1362            if ($isDefinedTest) {
1363                return false;
1364            }
1365
1366            if ($ignoreStrictCheck || !$env->isStrictVariables()) {
1367                return;
1368            }
1369
1370            if ($object instanceof ArrayAccess) {
1371                $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object));
1372            } elseif (\is_object($object)) {
1373                $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object));
1374            } elseif (\is_array($object)) {
1375                if (empty($object)) {
1376                    $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem);
1377                } else {
1378                    $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object)));
1379                }
1380            } elseif (/* Template::ARRAY_CALL */ 'array' === $type) {
1381                if (null === $object) {
1382                    $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item);
1383                } else {
1384                    $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
1385                }
1386            } elseif (null === $object) {
1387                $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item);
1388            } else {
1389                $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
1390            }
1391
1392            throw new RuntimeError($message, $lineno, $source);
1393        }
1394    }
1395
1396    if (!\is_object($object)) {
1397        if ($isDefinedTest) {
1398            return false;
1399        }
1400
1401        if ($ignoreStrictCheck || !$env->isStrictVariables()) {
1402            return;
1403        }
1404
1405        if (null === $object) {
1406            $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item);
1407        } elseif (\is_array($object)) {
1408            $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item);
1409        } else {
1410            $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
1411        }
1412
1413        throw new RuntimeError($message, $lineno, $source);
1414    }
1415
1416    if ($object instanceof Template) {
1417        throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source);
1418    }
1419
1420    // object property
1421    if (/* Template::METHOD_CALL */ 'method' !== $type) {
1422        if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) {
1423            if ($isDefinedTest) {
1424                return true;
1425            }
1426
1427            if ($sandboxed) {
1428                $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source);
1429            }
1430
1431            return $object->$item;
1432        }
1433    }
1434
1435    static $cache = [];
1436
1437    $class = \get_class($object);
1438
1439    // object method
1440    // precedence: getXxx() > isXxx() > hasXxx()
1441    if (!isset($cache[$class])) {
1442        $methods = get_class_methods($object);
1443        sort($methods);
1444        $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods);
1445        $classCache = [];
1446        foreach ($methods as $i => $method) {
1447            $classCache[$method] = $method;
1448            $classCache[$lcName = $lcMethods[$i]] = $method;
1449
1450            if ('g' === $lcName[0] && 0 === strpos($lcName, 'get')) {
1451                $name = substr($method, 3);
1452                $lcName = substr($lcName, 3);
1453            } elseif ('i' === $lcName[0] && 0 === strpos($lcName, 'is')) {
1454                $name = substr($method, 2);
1455                $lcName = substr($lcName, 2);
1456            } elseif ('h' === $lcName[0] && 0 === strpos($lcName, 'has')) {
1457                $name = substr($method, 3);
1458                $lcName = substr($lcName, 3);
1459                if (\in_array('is'.$lcName, $lcMethods)) {
1460                    continue;
1461                }
1462            } else {
1463                continue;
1464            }
1465
1466            // skip get() and is() methods (in which case, $name is empty)
1467            if ($name) {
1468                if (!isset($classCache[$name])) {
1469                    $classCache[$name] = $method;
1470                }
1471
1472                if (!isset($classCache[$lcName])) {
1473                    $classCache[$lcName] = $method;
1474                }
1475            }
1476        }
1477        $cache[$class] = $classCache;
1478    }
1479
1480    $call = false;
1481    if (isset($cache[$class][$item])) {
1482        $method = $cache[$class][$item];
1483    } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) {
1484        $method = $cache[$class][$lcItem];
1485    } elseif (isset($cache[$class]['__call'])) {
1486        $method = $item;
1487        $call = true;
1488    } else {
1489        if ($isDefinedTest) {
1490            return false;
1491        }
1492
1493        if ($ignoreStrictCheck || !$env->isStrictVariables()) {
1494            return;
1495        }
1496
1497        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);
1498    }
1499
1500    if ($isDefinedTest) {
1501        return true;
1502    }
1503
1504    if ($sandboxed) {
1505        $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source);
1506    }
1507
1508    // Some objects throw exceptions when they have __call, and the method we try
1509    // to call is not supported. If ignoreStrictCheck is true, we should return null.
1510    try {
1511        $ret = $object->$method(...$arguments);
1512    } catch (\BadMethodCallException $e) {
1513        if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) {
1514            return;
1515        }
1516        throw $e;
1517    }
1518
1519    return $ret;
1520}
1521
1522/**
1523 * Returns the values from a single column in the input array.
1524 *
1525 * <pre>
1526 *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
1527 *
1528 *  {% set fruits = items|column('fruit') %}
1529 *
1530 *  {# fruits now contains ['apple', 'orange'] #}
1531 * </pre>
1532 *
1533 * @param array|Traversable $array An array
1534 * @param mixed             $name  The column name
1535 * @param mixed             $index The column to use as the index/keys for the returned array
1536 *
1537 * @return array The array of values
1538 */
1539function twig_array_column($array, $name, $index = null): array
1540{
1541    if ($array instanceof Traversable) {
1542        $array = iterator_to_array($array);
1543    } elseif (!\is_array($array)) {
1544        throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
1545    }
1546
1547    return array_column($array, $name, $index);
1548}
1549
1550function twig_array_filter(Environment $env, $array, $arrow)
1551{
1552    if (!twig_test_iterable($array)) {
1553        throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array)));
1554    }
1555
1556    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
1557        throw new RuntimeError('The callable passed to "filter" filter must be a Closure in sandbox mode.');
1558    }
1559
1560    if (\is_array($array)) {
1561        return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
1562    }
1563
1564    // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
1565    return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
1566}
1567
1568function twig_array_map(Environment $env, $array, $arrow)
1569{
1570    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
1571        throw new RuntimeError('The callable passed to the "map" filter must be a Closure in sandbox mode.');
1572    }
1573
1574    $r = [];
1575    foreach ($array as $k => $v) {
1576        $r[$k] = $arrow($v, $k);
1577    }
1578
1579    return $r;
1580}
1581
1582function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
1583{
1584    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
1585        throw new RuntimeError('The callable passed to the "reduce" filter must be a Closure in sandbox mode.');
1586    }
1587
1588    if (!\is_array($array)) {
1589        if (!$array instanceof \Traversable) {
1590            throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
1591        }
1592
1593        $array = iterator_to_array($array);
1594    }
1595
1596    return array_reduce($array, $arrow, $initial);
1597}
1598}
1599