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