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