vendor/twig/twig/src/Extension/CoreExtension.php line 1753

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