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