diff --git a/composer.json b/composer.json index bceeedd32..1e422dbba 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ }, "autoload": { "files": [ + "src/Resources/core.php", "src/Resources/debug.php", "src/Resources/escaper.php", "src/Resources/string_loader.php" diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 142d25e10..13d4a4e47 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -21,6 +21,7 @@ use Symfony\Component\Intl\Timezones; use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; +use Twig\Extension\CoreExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -368,7 +369,7 @@ final class IntlExtension extends AbstractExtension */ public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string { - $date = twig_date_converter($env, $date, $timezone); + $date = CoreExtension::dateConverter($env, $date, $timezone); $formatterTimezone = $timezone; if (null === $formatterTimezone) { diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 36aa8f10a..8c9d0d4fe 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -9,8 +9,14 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; use Twig\ExpressionParser; +use Twig\Extension\SandboxExtension; +use Twig\Markup; use Twig\Node\Expression\Binary\AddBinary; use Twig\Node\Expression\Binary\AndBinary; use Twig\Node\Expression\Binary\BitwiseAndBinary; @@ -52,6 +58,9 @@ use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\NodeVisitor\MacroAutoImportNodeVisitor; +use Twig\Source; +use Twig\Template; +use Twig\TemplateWrapper; use Twig\TokenParser\ApplyTokenParser; use Twig\TokenParser\BlockTokenParser; use Twig\TokenParser\DeprecatedTokenParser; @@ -177,50 +186,50 @@ final class CoreExtension extends AbstractExtension { return [ // formatting filters - new TwigFilter('date', 'twig_date_format_filter', ['needs_environment' => true]), - new TwigFilter('date_modify', 'twig_date_modify_filter', ['needs_environment' => true]), - new TwigFilter('format', 'twig_sprintf'), - new TwigFilter('replace', 'twig_replace_filter'), - new TwigFilter('number_format', 'twig_number_format_filter', ['needs_environment' => true]), + new TwigFilter('date', [self::class, 'dateFormatFilter'], ['needs_environment' => true]), + new TwigFilter('date_modify', [self::class, 'dateModifyFilter'], ['needs_environment' => true]), + new TwigFilter('format', [self::class, 'sprintf']), + new TwigFilter('replace', [self::class, 'replaceFilter']), + new TwigFilter('number_format', [self::class, 'numberFormatFilter'], ['needs_environment' => true]), new TwigFilter('abs', 'abs'), - new TwigFilter('round', 'twig_round'), + new TwigFilter('round', [self::class, 'round']), // encoding - new TwigFilter('url_encode', 'twig_urlencode_filter'), + new TwigFilter('url_encode', [self::class, 'urlencodeFilter']), new TwigFilter('json_encode', 'json_encode'), - new TwigFilter('convert_encoding', 'twig_convert_encoding'), + new TwigFilter('convert_encoding', [self::class, 'convertEncoding']), // string filters - new TwigFilter('title', 'twig_title_string_filter', ['needs_environment' => true]), - new TwigFilter('capitalize', 'twig_capitalize_string_filter', ['needs_environment' => true]), - new TwigFilter('upper', 'twig_upper_filter', ['needs_environment' => true]), - new TwigFilter('lower', 'twig_lower_filter', ['needs_environment' => true]), - new TwigFilter('striptags', 'twig_striptags'), - new TwigFilter('trim', 'twig_trim_filter'), - new TwigFilter('nl2br', 'twig_nl2br', ['pre_escape' => 'html', 'is_safe' => ['html']]), - new TwigFilter('spaceless', 'twig_spaceless', ['is_safe' => ['html']]), + new TwigFilter('title', [self::class, 'titleStringFilter'], ['needs_environment' => true]), + new TwigFilter('capitalize', [self::class, 'capitalizeStringFilter'], ['needs_environment' => true]), + new TwigFilter('upper', [self::class, 'upperFilter'], ['needs_environment' => true]), + new TwigFilter('lower', [self::class, 'lowerFilter'], ['needs_environment' => true]), + new TwigFilter('striptags', [self::class, 'striptags']), + new TwigFilter('trim', [self::class, 'trimFilter']), + new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]), + new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html']]), // array helpers - new TwigFilter('join', 'twig_join_filter'), - new TwigFilter('split', 'twig_split_filter', ['needs_environment' => true]), - new TwigFilter('sort', 'twig_sort_filter', ['needs_environment' => true]), - new TwigFilter('merge', 'twig_array_merge'), - new TwigFilter('batch', 'twig_array_batch'), - new TwigFilter('column', 'twig_array_column'), - new TwigFilter('filter', 'twig_array_filter', ['needs_environment' => true]), - new TwigFilter('map', 'twig_array_map', ['needs_environment' => true]), - new TwigFilter('reduce', 'twig_array_reduce', ['needs_environment' => true]), + new TwigFilter('join', [self::class, 'joinFilter']), + new TwigFilter('split', [self::class, 'splitFilter'], ['needs_environment' => true]), + new TwigFilter('sort', [self::class, 'sortFilter'], ['needs_environment' => true]), + new TwigFilter('merge', [self::class, 'arrayMerge']), + new TwigFilter('batch', [self::class, 'arrayBatch']), + new TwigFilter('column', [self::class, 'arrayColumn']), + new TwigFilter('filter', [self::class, 'arrayFilter'], ['needs_environment' => true]), + new TwigFilter('map', [self::class, 'arrayMap'], ['needs_environment' => true]), + new TwigFilter('reduce', [self::class, 'arrayReduce'], ['needs_environment' => true]), // string/array filters - new TwigFilter('reverse', 'twig_reverse_filter', ['needs_environment' => true]), - new TwigFilter('length', 'twig_length_filter', ['needs_environment' => true]), - new TwigFilter('slice', 'twig_slice', ['needs_environment' => true]), - new TwigFilter('first', 'twig_first', ['needs_environment' => true]), - new TwigFilter('last', 'twig_last', ['needs_environment' => true]), + new TwigFilter('reverse', [self::class, 'reverseFilter'], ['needs_environment' => true]), + new TwigFilter('length', [self::class, 'lengthFilter'], ['needs_environment' => true]), + new TwigFilter('slice', [self::class, 'slice'], ['needs_environment' => true]), + new TwigFilter('first', [self::class, 'first'], ['needs_environment' => true]), + new TwigFilter('last', [self::class, 'last'], ['needs_environment' => true]), // iteration and runtime - new TwigFilter('default', '_twig_default_filter', ['node_class' => DefaultFilter::class]), - new TwigFilter('keys', 'twig_get_array_keys_filter'), + new TwigFilter('default', [self::class, 'defaultFilter'], ['node_class' => DefaultFilter::class]), + new TwigFilter('keys', [self::class, 'getArrayKeysFilter']), ]; } @@ -230,12 +239,12 @@ final class CoreExtension extends AbstractExtension new TwigFunction('max', 'max'), new TwigFunction('min', 'min'), new TwigFunction('range', 'range'), - new TwigFunction('constant', 'twig_constant'), - new TwigFunction('cycle', 'twig_cycle'), - new TwigFunction('random', 'twig_random', ['needs_environment' => true]), - new TwigFunction('date', 'twig_date_converter', ['needs_environment' => true]), - new TwigFunction('include', 'twig_include', ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), - new TwigFunction('source', 'twig_source', ['needs_environment' => true, 'is_safe' => ['all']]), + new TwigFunction('constant', [self::class, 'constant']), + new TwigFunction('cycle', [self::class, 'cycle']), + new TwigFunction('random', [self::class, 'random'], ['needs_environment' => true]), + new TwigFunction('date', [self::class, 'dateConverter'], ['needs_environment' => true]), + new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), + new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]), ]; } @@ -250,7 +259,7 @@ final class CoreExtension extends AbstractExtension new TwigTest('null', null, ['node_class' => NullTest::class]), new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]), new TwigTest('constant', null, ['node_class' => ConstantTest::class]), - new TwigTest('empty', 'twig_test_empty'), + new TwigTest('empty', [self::class, 'testEmpty']), new TwigTest('iterable', 'is_iterable'), ]; } @@ -303,192 +312,213 @@ final class CoreExtension extends AbstractExtension ], ]; } -} -} -namespace { - use Twig\Environment; - use Twig\Error\LoaderError; - use Twig\Error\RuntimeError; - use Twig\Extension\CoreExtension; - use Twig\Extension\SandboxExtension; - use Twig\Markup; - use Twig\Source; - use Twig\Template; - use Twig\TemplateWrapper; + /** + * Cycles over a value. + * + * @param \ArrayAccess|array $values + * @param int $position The cycle position + * + * @return string The next value in the cycle + * + * @internal + */ + public static function cycle($values, $position) + { + if (!\is_array($values) && !$values instanceof \ArrayAccess) { + return $values; + } -/** - * Cycles over a value. - * - * @param \ArrayAccess|array $values - * @param int $position The cycle position - * - * @return string The next value in the cycle - */ -function twig_cycle($values, $position) -{ - if (!\is_array($values) && !$values instanceof \ArrayAccess) { - return $values; + return $values[$position % \count($values)]; } - return $values[$position % \count($values)]; -} + /** + * Returns a random value depending on the supplied parameter type: + * - a random item from a \Traversable or array + * - a random character from a string + * - a random integer between 0 and the integer parameter. + * + * @param \Traversable|array|int|float|string $values The values to pick a random item from + * @param int|null $max Maximum value used when $values is an int + * + * @return mixed A random value from the given sequence + * + * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) + * + * @internal + */ + public static function random(Environment $env, $values = null, $max = null) + { + if (null === $values) { + return null === $max ? mt_rand() : mt_rand(0, (int) $max); + } -/** - * Returns a random value depending on the supplied parameter type: - * - a random item from a \Traversable or array - * - a random character from a string - * - a random integer between 0 and the integer parameter. - * - * @param \Traversable|array|int|float|string $values The values to pick a random item from - * @param int|null $max Maximum value used when $values is an int - * - * @return mixed A random value from the given sequence - * - * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) - */ -function twig_random(Environment $env, $values = null, $max = null) -{ - if (null === $values) { - return null === $max ? mt_rand() : mt_rand(0, (int) $max); - } - - if (\is_int($values) || \is_float($values)) { - if (null === $max) { - if ($values < 0) { - $max = 0; - $min = $values; + if (\is_int($values) || \is_float($values)) { + if (null === $max) { + if ($values < 0) { + $max = 0; + $min = $values; + } else { + $max = $values; + $min = 0; + } } else { - $max = $values; - $min = 0; + $min = $values; } + + return mt_rand((int) $min, (int) $max); + } + + if (\is_string($values)) { + if ('' === $values) { + return ''; + } + + $charset = $env->getCharset(); + + if ('UTF-8' !== $charset) { + $values = self::convertEncoding($values, 'UTF-8', $charset); + } + + // unicode version of str_split() + // split at all positions, but not after the start and not before the end + $values = preg_split('/(? $value) { + $values[$i] = self::convertEncoding($value, $charset, 'UTF-8'); + } + } + } + + if (!is_iterable($values)) { + return $values; + } + + $values = self::toArray($values); + + if (0 === \count($values)) { + throw new RuntimeError('The random function cannot pick from an empty array.'); + } + + return $values[array_rand($values, 1)]; + } + + /** + * Converts a date to the given format. + * + * {{ post.published_at|date("m/d/Y") }} + * + * @param \DateTimeInterface|\DateInterval|string $date A date + * @param string|null $format The target format, null to use the default + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return string The formatted date + * + * @internal + */ + public static function dateFormatFilter(Environment $env, $date, $format = null, $timezone = null) + { + if (null === $format) { + $formats = $env->getExtension(self::class)->getDateFormat(); + $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; + } + + if ($date instanceof \DateInterval) { + return $date->format($format); + } + + return self::dateConverter($env, $date, $timezone)->format($format); + } + + /** + * Returns a new date object modified. + * + * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} + * + * @param \DateTimeInterface|string $date A date + * @param string $modifier A modifier string + * + * @return \DateTimeInterface + * + * @internal + */ + public static function dateModifyFilter(Environment $env, $date, $modifier) + { + $date = self::dateConverter($env, $date, false); + + return $date->modify($modifier); + } + + /** + * Returns a formatted string. + * + * @param string|null $format + * @param ...$values + * + * @return string + * + * @internal + */ + public static function sprintf($format, ...$values) + { + return sprintf($format ?? '', ...$values); + } + + /** + * Converts an input to a \DateTime instance. + * + * {% if date(user.created_at) < date('+2days') %} + * {# do something #} + * {% endif %} + * + * @param \DateTimeInterface|string|null $date A date or null to use the current time + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return \DateTimeInterface + * + * @internal + */ + public static function dateConverter(Environment $env, $date = null, $timezone = null) + { + // determine the timezone + if (false !== $timezone) { + if (null === $timezone) { + $timezone = $env->getExtension(self::class)->getTimezone(); + } elseif (!$timezone instanceof \DateTimeZone) { + $timezone = new \DateTimeZone($timezone); + } + } + + // immutable dates + if ($date instanceof \DateTimeImmutable) { + return false !== $timezone ? $date->setTimezone($timezone) : $date; + } + + if ($date instanceof \DateTimeInterface) { + $date = clone $date; + if (false !== $timezone) { + $date->setTimezone($timezone); + } + + return $date; + } + + if (null === $date || 'now' === $date) { + if (null === $date) { + $date = 'now'; + } + + return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(self::class)->getTimezone()); + } + + $asString = (string) $date; + if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + $date = new \DateTime('@'.$date); } else { - $min = $values; + $date = new \DateTime($date, $env->getExtension(self::class)->getTimezone()); } - return mt_rand((int) $min, (int) $max); - } - - if (\is_string($values)) { - if ('' === $values) { - return ''; - } - - $charset = $env->getCharset(); - - if ('UTF-8' !== $charset) { - $values = twig_convert_encoding($values, 'UTF-8', $charset); - } - - // unicode version of str_split() - // split at all positions, but not after the start and not before the end - $values = preg_split('/(? $value) { - $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8'); - } - } - } - - if (!is_iterable($values)) { - return $values; - } - - $values = twig_to_array($values); - - if (0 === \count($values)) { - throw new RuntimeError('The random function cannot pick from an empty array.'); - } - - return $values[array_rand($values, 1)]; -} - -/** - * Converts a date to the given format. - * - * {{ post.published_at|date("m/d/Y") }} - * - * @param \DateTimeInterface|\DateInterval|string $date A date - * @param string|null $format The target format, null to use the default - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return string The formatted date - */ -function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) -{ - if (null === $format) { - $formats = $env->getExtension(CoreExtension::class)->getDateFormat(); - $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; - } - - if ($date instanceof \DateInterval) { - return $date->format($format); - } - - return twig_date_converter($env, $date, $timezone)->format($format); -} - -/** - * Returns a new date object modified. - * - * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} - * - * @param \DateTimeInterface|string $date A date - * @param string $modifier A modifier string - * - * @return \DateTimeInterface - */ -function twig_date_modify_filter(Environment $env, $date, $modifier) -{ - $date = twig_date_converter($env, $date, false); - - return $date->modify($modifier); -} - -/** - * Returns a formatted string. - * - * @param string|null $format - * @param ...$values - * - * @return string - */ -function twig_sprintf($format, ...$values) -{ - return sprintf($format ?? '', ...$values); -} - -/** - * Converts an input to a \DateTime instance. - * - * {% if date(user.created_at) < date('+2days') %} - * {# do something #} - * {% endif %} - * - * @param \DateTimeInterface|string|null $date A date or null to use the current time - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return \DateTimeInterface - */ -function twig_date_converter(Environment $env, $date = null, $timezone = null) -{ - // determine the timezone - if (false !== $timezone) { - if (null === $timezone) { - $timezone = $env->getExtension(CoreExtension::class)->getTimezone(); - } elseif (!$timezone instanceof \DateTimeZone) { - $timezone = new \DateTimeZone($timezone); - } - } - - // immutable dates - if ($date instanceof \DateTimeImmutable) { - return false !== $timezone ? $date->setTimezone($timezone) : $date; - } - - if ($date instanceof \DateTimeInterface) { - $date = clone $date; if (false !== $timezone) { $date->setTimezone($timezone); } @@ -496,993 +526,1068 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null) return $date; } - if (null === $date || 'now' === $date) { - if (null === $date) { - $date = 'now'; + /** + * Replaces strings within a string. + * + * @param string|null $str String to replace in + * @param array|\Traversable $from Replace values + * + * @return string + * + * @internal + */ + public static function replaceFilter($str, $from) + { + if (!is_iterable($from)) { + throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); } - return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone()); + return strtr($str ?? '', self::toArray($from)); } - $asString = (string) $date; - if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { - $date = new \DateTime('@'.$date); - } else { - $date = new \DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone()); - } + /** + * Rounds a number. + * + * @param int|float|string|null $value The value to round + * @param int|float $precision The rounding precision + * @param string $method The method to use for rounding + * + * @return int|float The rounded number + * + * @internal + */ + public static function round($value, $precision = 0, $method = 'common') + { + $value = (float) $value; - if (false !== $timezone) { - $date->setTimezone($timezone); - } - - return $date; -} - -/** - * Replaces strings within a string. - * - * @param string|null $str String to replace in - * @param array|\Traversable $from Replace values - * - * @return string - */ -function twig_replace_filter($str, $from) -{ - if (!is_iterable($from)) { - throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); - } - - return strtr($str ?? '', twig_to_array($from)); -} - -/** - * Rounds a number. - * - * @param int|float|string|null $value The value to round - * @param int|float $precision The rounding precision - * @param string $method The method to use for rounding - * - * @return int|float The rounded number - */ -function twig_round($value, $precision = 0, $method = 'common') -{ - $value = (float) $value; - - if ('common' === $method) { - return round($value, $precision); - } - - if ('ceil' !== $method && 'floor' !== $method) { - throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); - } - - return $method($value * 10 ** $precision) / 10 ** $precision; -} - -/** - * Number format filter. - * - * All of the formatting options can be left null, in that case the defaults will - * be used. Supplying any of the parameters will override the defaults set in the - * environment object. - * - * @param mixed $number A float/int/string of the number to format - * @param int $decimal the number of decimal points to display - * @param string $decimalPoint the character(s) to use for the decimal point - * @param string $thousandSep the character(s) to use for the thousands separator - * - * @return string The formatted number - */ -function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) -{ - $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat(); - if (null === $decimal) { - $decimal = $defaults[0]; - } - - if (null === $decimalPoint) { - $decimalPoint = $defaults[1]; - } - - if (null === $thousandSep) { - $thousandSep = $defaults[2]; - } - - return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); -} - -/** - * URL encodes (RFC 3986) a string as a path segment or an array as a query string. - * - * @param string|array|null $url A URL or an array of query parameters - * - * @return string The URL encoded value - */ -function twig_urlencode_filter($url) -{ - if (\is_array($url)) { - return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); - } - - return rawurlencode($url ?? ''); -} - -/** - * Merges any number of arrays or Traversable objects. - * - * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} - * - * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %} - * - * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #} - * - * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge - * - * @return array The merged array - */ -function twig_array_merge(...$arrays) -{ - $result = []; - - foreach ($arrays as $argNumber => $array) { - if (!is_iterable($array)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); + if ('common' === $method) { + return round($value, $precision); } - $result = array_merge($result, twig_to_array($array)); - } - - return $result; -} - - -/** - * Slices a variable. - * - * @param mixed $item A variable - * @param int $start Start of the slice - * @param int $length Size of the slice - * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) - * - * @return mixed The sliced variable - */ -function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - while ($item instanceof \IteratorAggregate) { - $item = $item->getIterator(); + if ('ceil' !== $method && 'floor' !== $method) { + throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); } - if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { - try { - return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys); - } catch (\OutOfBoundsException $e) { - return []; + return $method($value * 10 ** $precision) / 10 ** $precision; + } + + /** + * Number format filter. + * + * All of the formatting options can be left null, in that case the defaults will + * be used. Supplying any of the parameters will override the defaults set in the + * environment object. + * + * @param mixed $number A float/int/string of the number to format + * @param int $decimal the number of decimal points to display + * @param string $decimalPoint the character(s) to use for the decimal point + * @param string $thousandSep the character(s) to use for the thousands separator + * + * @return string The formatted number + * + * @internal + */ + public static function numberFormatFilter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) + { + $defaults = $env->getExtension(self::class)->getNumberFormat(); + if (null === $decimal) { + $decimal = $defaults[0]; + } + + if (null === $decimalPoint) { + $decimalPoint = $defaults[1]; + } + + if (null === $thousandSep) { + $thousandSep = $defaults[2]; + } + + return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); + } + + /** + * URL encodes (RFC 3986) a string as a path segment or an array as a query string. + * + * @param string|array|null $url A URL or an array of query parameters + * + * @return string The URL encoded value + * + * @internal + */ + public static function urlencodeFilter($url) + { + if (\is_array($url)) { + return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); + } + + return rawurlencode($url ?? ''); + } + + /** + * Merges any number of arrays or Traversable objects. + * + * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} + * + * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %} + * + * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #} + * + * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge + * + * @return array The merged array + * + * @internal + */ + public static function arrayMerge(...$arrays) + { + $result = []; + + foreach ($arrays as $argNumber => $array) { + if (!is_iterable($array)) { + throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); } + + $result = array_merge($result, self::toArray($array)); } - $item = iterator_to_array($item, $preserveKeys); + return $result; } - if (\is_array($item)) { - return \array_slice($item, $start, $length, $preserveKeys); - } - return mb_substr((string) $item, $start, $length, $env->getCharset()); -} + /** + * Slices a variable. + * + * @param mixed $item A variable + * @param int $start Start of the slice + * @param int $length Size of the slice + * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) + * + * @return mixed The sliced variable + * + * @internal + */ + public static function slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + while ($item instanceof \IteratorAggregate) { + $item = $item->getIterator(); + } -/** - * Returns the first element of the item. - * - * @param mixed $item A variable - * - * @return mixed The first element of the item - */ -function twig_first(Environment $env, $item) -{ - $elements = twig_slice($env, $item, 0, 1, false); + if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { + try { + return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys); + } catch (\OutOfBoundsException $e) { + return []; + } + } - return \is_string($elements) ? $elements : current($elements); -} - -/** - * Returns the last element of the item. - * - * @param mixed $item A variable - * - * @return mixed The last element of the item - */ -function twig_last(Environment $env, $item) -{ - $elements = twig_slice($env, $item, -1, 1, false); - - return \is_string($elements) ? $elements : current($elements); -} - -/** - * Joins the values to a string. - * - * The separators between elements are empty strings per default, you can define them with the optional parameters. - * - * {{ [1, 2, 3]|join(', ', ' and ') }} - * {# returns 1, 2 and 3 #} - * - * {{ [1, 2, 3]|join('|') }} - * {# returns 1|2|3 #} - * - * {{ [1, 2, 3]|join }} - * {# returns 123 #} - * - * @param array $value An array - * @param string $glue The separator - * @param string|null $and The separator for the last pair - * - * @return string The concatenated string - */ -function twig_join_filter($value, $glue = '', $and = null) -{ - if (!is_iterable($value)) { - $value = (array) $value; - } - - $value = twig_to_array($value, false); - - if (0 === \count($value)) { - return ''; - } - - if (null === $and || $and === $glue) { - return implode($glue, $value); - } - - if (1 === \count($value)) { - return $value[0]; - } - - return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; -} - -/** - * Splits the string into an array. - * - * {{ "one,two,three"|split(',') }} - * {# returns [one, two, three] #} - * - * {{ "one,two,three,four,five"|split(',', 3) }} - * {# returns [one, two, "three,four,five"] #} - * - * {{ "123"|split('') }} - * {# returns [1, 2, 3] #} - * - * {{ "aabbcc"|split('', 2) }} - * {# returns [aa, bb, cc] #} - * - * @param string|null $value A string - * @param string $delimiter The delimiter - * @param int $limit The limit - * - * @return array The split string as an array - */ -function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) -{ - $value = $value ?? ''; - - if ('' !== $delimiter) { - return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); - } - - if ($limit <= 1) { - return preg_split('/(?getCharset()); - if ($length < $limit) { - return [$value]; - } - - $r = []; - for ($i = 0; $i < $length; $i += $limit) { - $r[] = mb_substr($value, $i, $limit, $env->getCharset()); - } - - return $r; -} - -// The '_default' filter is used internally to avoid using the ternary operator -// which costs a lot for big contexts (before PHP 5.4). So, on average, -// a function call is cheaper. -/** - * @internal - */ -function _twig_default_filter($value, $default = '') -{ - if (twig_test_empty($value)) { - return $default; - } - - return $value; -} - -/** - * Returns the keys for the given array. - * - * It is useful when you want to iterate over the keys of an array: - * - * {% for key in array|keys %} - * {# ... #} - * {% endfor %} - * - * @param array $array An array - * - * @return array The keys - */ -function twig_get_array_keys_filter($array) -{ - if ($array instanceof \Traversable) { - while ($array instanceof \IteratorAggregate) { - $array = $array->getIterator(); + $item = iterator_to_array($item, $preserveKeys); } - $keys = []; - if ($array instanceof \Iterator) { - $array->rewind(); - while ($array->valid()) { - $keys[] = $array->key(); - $array->next(); + if (\is_array($item)) { + return \array_slice($item, $start, $length, $preserveKeys); + } + + return mb_substr((string) $item, $start, $length, $env->getCharset()); + } + + /** + * Returns the first element of the item. + * + * @param mixed $item A variable + * + * @return mixed The first element of the item + * + * @internal + */ + public static function first(Environment $env, $item) + { + $elements = self::slice($env, $item, 0, 1, false); + + return \is_string($elements) ? $elements : current($elements); + } + + /** + * Returns the last element of the item. + * + * @param mixed $item A variable + * + * @return mixed The last element of the item + * + * @internal + */ + public static function last(Environment $env, $item) + { + $elements = self::slice($env, $item, -1, 1, false); + + return \is_string($elements) ? $elements : current($elements); + } + + /** + * Joins the values to a string. + * + * The separators between elements are empty strings per default, you can define them with the optional parameters. + * + * {{ [1, 2, 3]|join(', ', ' and ') }} + * {# returns 1, 2 and 3 #} + * + * {{ [1, 2, 3]|join('|') }} + * {# returns 1|2|3 #} + * + * {{ [1, 2, 3]|join }} + * {# returns 123 #} + * + * @param array $value An array + * @param string $glue The separator + * @param string|null $and The separator for the last pair + * + * @return string The concatenated string + * + * @internal + */ + public static function joinFilter($value, $glue = '', $and = null) + { + if (!is_iterable($value)) { + $value = (array) $value; + } + + $value = self::toArray($value, false); + + if (0 === \count($value)) { + return ''; + } + + if (null === $and || $and === $glue) { + return implode($glue, $value); + } + + if (1 === \count($value)) { + return $value[0]; + } + + return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; + } + + /** + * Splits the string into an array. + * + * {{ "one,two,three"|split(',') }} + * {# returns [one, two, three] #} + * + * {{ "one,two,three,four,five"|split(',', 3) }} + * {# returns [one, two, "three,four,five"] #} + * + * {{ "123"|split('') }} + * {# returns [1, 2, 3] #} + * + * {{ "aabbcc"|split('', 2) }} + * {# returns [aa, bb, cc] #} + * + * @param string|null $value A string + * @param string $delimiter The delimiter + * @param int $limit The limit + * + * @return array The split string as an array + * + * @internal + */ + public static function splitFilter(Environment $env, $value, $delimiter, $limit = null) + { + $value = $value ?? ''; + + if ('' !== $delimiter) { + return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); + } + + if ($limit <= 1) { + return preg_split('/(?getCharset()); + if ($length < $limit) { + return [$value]; + } + + $r = []; + for ($i = 0; $i < $length; $i += $limit) { + $r[] = mb_substr($value, $i, $limit, $env->getCharset()); + } + + return $r; + } + + // The '_default' filter is used internally to avoid using the ternary operator + // which costs a lot for big contexts (before PHP 5.4). So, on average, + // a function call is cheaper. + /** + * @internal + */ + public static function defaultFilter($value, $default = '') + { + if (self::testEmpty($value)) { + return $default; + } + + return $value; + } + + /** + * Returns the keys for the given array. + * + * It is useful when you want to iterate over the keys of an array: + * + * {% for key in array|keys %} + * {# ... #} + * {% endfor %} + * + * @param array $array An array + * + * @return array The keys + * + * @internal + */ + public static function getArrayKeysFilter($array) + { + if ($array instanceof \Traversable) { + while ($array instanceof \IteratorAggregate) { + $array = $array->getIterator(); + } + + $keys = []; + if ($array instanceof \Iterator) { + $array->rewind(); + while ($array->valid()) { + $keys[] = $array->key(); + $array->next(); + } + + return $keys; + } + + foreach ($array as $key => $item) { + $keys[] = $key; } return $keys; } - foreach ($array as $key => $item) { - $keys[] = $key; + if (!\is_array($array)) { + return []; } - return $keys; + return array_keys($array); } - if (!\is_array($array)) { - return []; - } - - return array_keys($array); -} - -/** - * Reverses a variable. - * - * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string - * @param bool $preserveKeys Whether to preserve key or not - * - * @return mixed The reversed input - */ -function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - return array_reverse(iterator_to_array($item), $preserveKeys); - } - - if (\is_array($item)) { - return array_reverse($item, $preserveKeys); - } - - $string = (string) $item; - - $charset = $env->getCharset(); - - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } - - preg_match_all('/./us', $string, $matches); - - $string = implode('', array_reverse($matches[0])); - - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, $charset, 'UTF-8'); - } - - return $string; -} - -/** - * Sorts an array. - * - * @param array|\Traversable $array - * - * @return array - */ -function twig_sort_filter(Environment $env, $array, $arrow = null) -{ - if ($array instanceof \Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); - } - - if (null !== $arrow) { - twig_check_arrow_in_sandbox($env, $arrow, 'sort', 'filter'); - - uasort($array, $arrow); - } else { - asort($array); - } - - return $array; -} - -/** - * @internal - */ -function twig_in_filter($value, $compare) -{ - if ($value instanceof Markup) { - $value = (string) $value; - } - if ($compare instanceof Markup) { - $compare = (string) $compare; - } - - if (\is_string($compare)) { - if (\is_string($value) || \is_int($value) || \is_float($value)) { - return '' === $value || str_contains($compare, (string) $value); + /** + * Reverses a variable. + * + * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string + * @param bool $preserveKeys Whether to preserve key or not + * + * @return mixed The reversed input + * + * @internal + */ + public static function reverseFilter(Environment $env, $item, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + return array_reverse(iterator_to_array($item), $preserveKeys); } - return false; + if (\is_array($item)) { + return array_reverse($item, $preserveKeys); + } + + $string = (string) $item; + + $charset = $env->getCharset(); + + if ('UTF-8' !== $charset) { + $string = self::convertEncoding($string, 'UTF-8', $charset); + } + + preg_match_all('/./us', $string, $matches); + + $string = implode('', array_reverse($matches[0])); + + if ('UTF-8' !== $charset) { + $string = self::convertEncoding($string, $charset, 'UTF-8'); + } + + return $string; } - if (!is_iterable($compare)) { - return false; + /** + * Sorts an array. + * + * @param array|\Traversable $array + * + * @return array + * + * @internal + */ + public static function sortFilter(Environment $env, $array, $arrow = null) + { + if ($array instanceof \Traversable) { + $array = iterator_to_array($array); + } elseif (!\is_array($array)) { + throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); + } + + if (null !== $arrow) { + self::checkArrowInSandbox($env, $arrow, 'sort', 'filter'); + + uasort($array, $arrow); + } else { + asort($array); + } + + return $array; } - if (\is_object($value) || \is_resource($value)) { - if (!\is_array($compare)) { - foreach ($compare as $item) { - if ($item === $value) { - return true; - } + /** + * @internal + */ + public static function inFilter($value, $compare) + { + if ($value instanceof Markup) { + $value = (string) $value; + } + if ($compare instanceof Markup) { + $compare = (string) $compare; + } + + if (\is_string($compare)) { + if (\is_string($value) || \is_int($value) || \is_float($value)) { + return '' === $value || str_contains($compare, (string) $value); } return false; } - return \in_array($value, $compare, true); - } - - foreach ($compare as $item) { - if (0 === twig_compare($value, $item)) { - return true; - } - } - - return false; -} - -/** - * Compares two values using a more strict version of the PHP non-strict comparison operator. - * - * @see https://wiki.php.net/rfc/string_to_number_comparison - * @see https://wiki.php.net/rfc/trailing_whitespace_numerics - * - * @internal - */ -function twig_compare($a, $b) -{ - // int <=> string - if (\is_int($a) && \is_string($b)) { - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; - } - if ((int) $bTrim == $bTrim) { - return $a <=> (int) $bTrim; - } else { - return (float) $a <=> (float) $bTrim; - } - } - if (\is_string($a) && \is_int($b)) { - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; - } - if ((int) $aTrim == $aTrim) { - return (int) $aTrim <=> $b; - } else { - return (float) $aTrim <=> (float) $b; - } - } - - // float <=> string - if (\is_float($a) && \is_string($b)) { - if (is_nan($a)) { - return 1; - } - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; + if (!is_iterable($compare)) { + return false; } - return $a <=> (float) $bTrim; - } - if (\is_string($a) && \is_float($b)) { - if (is_nan($b)) { - return 1; - } - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; + if (\is_object($value) || \is_resource($value)) { + if (!\is_array($compare)) { + foreach ($compare as $item) { + if ($item === $value) { + return true; + } + } + + return false; + } + + return \in_array($value, $compare, true); } - return (float) $aTrim <=> $b; - } - - // fallback to <=> - return $a <=> $b; -} - -/** - * @return int - * - * @throws RuntimeError When an invalid pattern is used - */ -function twig_matches(string $regexp, ?string $str) -{ - set_error_handler(function ($t, $m) use ($regexp) { - throw new RuntimeError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); - }); - try { - return preg_match($regexp, $str ?? ''); - } finally { - restore_error_handler(); - } -} - -/** - * Returns a trimmed string. - * - * @param string|null $string - * @param string|null $characterMask - * @param string $side - * - * @return string - * - * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') - */ -function twig_trim_filter($string, $characterMask = null, $side = 'both') -{ - if (null === $characterMask) { - $characterMask = " \t\n\r\0\x0B"; - } - - switch ($side) { - case 'both': - return trim($string ?? '', $characterMask); - case 'left': - return ltrim($string ?? '', $characterMask); - case 'right': - return rtrim($string ?? '', $characterMask); - default: - throw new RuntimeError('Trimming side must be "left", "right" or "both".'); - } -} - -/** - * Inserts HTML line breaks before all newlines in a string. - * - * @param string|null $string - * - * @return string - */ -function twig_nl2br($string) -{ - return nl2br($string ?? ''); -} - -/** - * Removes whitespaces between HTML tags. - * - * @param string|null $string - * - * @return string - */ -function twig_spaceless($content) -{ - return trim(preg_replace('/>\s+<', $content ?? '')); -} - -/** - * @param string|null $string - * @param string $to - * @param string $from - * - * @return string - */ -function twig_convert_encoding($string, $to, $from) -{ - if (!\function_exists('iconv')) { - throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); - } - - return iconv($from, $to, $string ?? ''); -} - -/** - * Returns the length of a variable. - * - * @param mixed $thing A variable - * - * @return int The length of the value - */ -function twig_length_filter(Environment $env, $thing) -{ - if (null === $thing) { - return 0; - } - - if (\is_scalar($thing)) { - return mb_strlen($thing, $env->getCharset()); - } - - if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { - return \count($thing); - } - - if ($thing instanceof \Traversable) { - return iterator_count($thing); - } - - if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { - return mb_strlen((string) $thing, $env->getCharset()); - } - - return 1; -} - -/** - * Converts a string to uppercase. - * - * @param string|null $string A string - * - * @return string The uppercased string - */ -function twig_upper_filter(Environment $env, $string) -{ - return mb_strtoupper($string ?? '', $env->getCharset()); -} - -/** - * Converts a string to lowercase. - * - * @param string|null $string A string - * - * @return string The lowercased string - */ -function twig_lower_filter(Environment $env, $string) -{ - return mb_strtolower($string ?? '', $env->getCharset()); -} - -/** - * Strips HTML and PHP tags from a string. - * - * @param string|null $string - * @param string[]|string|null $string - * - * @return string - */ -function twig_striptags($string, $allowable_tags = null) -{ - return strip_tags($string ?? '', $allowable_tags); -} - -/** - * Returns a titlecased string. - * - * @param string|null $string A string - * - * @return string The titlecased string - */ -function twig_title_string_filter(Environment $env, $string) -{ - if (null !== $charset = $env->getCharset()) { - return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); - } - - return ucwords(strtolower($string ?? '')); -} - -/** - * Returns a capitalized string. - * - * @param string|null $string A string - * - * @return string The capitalized string - */ -function twig_capitalize_string_filter(Environment $env, $string) -{ - $charset = $env->getCharset(); - - return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); -} - -/** - * @internal - */ -function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) -{ - if (!method_exists($template, $method)) { - $parent = $template; - while ($parent = $parent->getParent($context)) { - if (method_exists($parent, $method)) { - return $parent->$method(...$args); + foreach ($compare as $item) { + if (0 === self::compare($value, $item)) { + return true; } } - throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); + return false; } - return $template->$method(...$args); -} - -/** - * @internal - */ -function twig_ensure_traversable($seq) -{ - if (is_iterable($seq)) { - return $seq; - } - - return []; -} - -/** - * @internal - */ -function twig_to_array($seq, $preserveKeys = true) -{ - if ($seq instanceof \Traversable) { - return iterator_to_array($seq, $preserveKeys); - } - - if (!\is_array($seq)) { - return $seq; - } - - return $preserveKeys ? $seq : array_values($seq); -} - -/** - * Checks if a variable is empty. - * - * {# evaluates to true if the foo variable is null, false, or the empty string #} - * {% if foo is empty %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is empty, false otherwise - */ -function twig_test_empty($value) -{ - if ($value instanceof \Countable) { - return 0 === \count($value); - } - - if ($value instanceof \Traversable) { - return !iterator_count($value); - } - - if (\is_object($value) && method_exists($value, '__toString')) { - return '' === (string) $value; - } - - return '' === $value || false === $value || null === $value || [] === $value; -} - -/** - * Checks if a variable is traversable. - * - * {# evaluates to true if the foo variable is an array or a traversable object #} - * {% if foo is iterable %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is traversable - * - * @deprecated since Twig 3.8, to be removed in 4.0 (use the native "is_iterable" function instead) - */ -function twig_test_iterable($value) -{ - return is_iterable($value); -} - -/** - * Renders a template. - * - * @param array $context - * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively - * @param array $variables The variables to pass to the template - * @param bool $withContext - * @param bool $ignoreMissing Whether to ignore missing templates or not - * @param bool $sandboxed Whether to sandbox the template or not - * - * @return string The rendered template - */ -function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) -{ - $alreadySandboxed = false; - $sandbox = null; - if ($withContext) { - $variables = array_merge($context, $variables); - } - - if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { - $sandbox = $env->getExtension(SandboxExtension::class); - if (!$alreadySandboxed = $sandbox->isSandboxed()) { - $sandbox->enableSandbox(); - } - - foreach ((\is_array($template) ? $template : [$template]) as $name) { - // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security - if ($name instanceof TemplateWrapper || $name instanceof Template) { - $name->unwrap()->checkSecurity(); + /** + * Compares two values using a more strict version of the PHP non-strict comparison operator. + * + * @see https://wiki.php.net/rfc/string_to_number_comparison + * @see https://wiki.php.net/rfc/trailing_whitespace_numerics + * + * @internal + */ + public static function compare($a, $b) + { + // int <=> string + if (\is_int($a) && \is_string($b)) { + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + if ((int) $bTrim == $bTrim) { + return $a <=> (int) $bTrim; + } else { + return (float) $a <=> (float) $bTrim; } } + if (\is_string($a) && \is_int($b)) { + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + if ((int) $aTrim == $aTrim) { + return (int) $aTrim <=> $b; + } else { + return (float) $aTrim <=> (float) $b; + } + } + + // float <=> string + if (\is_float($a) && \is_string($b)) { + if (is_nan($a)) { + return 1; + } + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + + return $a <=> (float) $bTrim; + } + if (\is_string($a) && \is_float($b)) { + if (is_nan($b)) { + return 1; + } + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + + return (float) $aTrim <=> $b; + } + + // fallback to <=> + return $a <=> $b; } - try { - $loaded = null; + /** + * @return int + * + * @throws RuntimeError When an invalid pattern is used + * + * @internal + */ + public static function matches(string $regexp, ?string $str) + { + set_error_handler(function ($t, $m) use ($regexp) { + throw new RuntimeError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); + }); try { - $loaded = $env->resolveTemplate($template); + return preg_match($regexp, $str ?? ''); + } finally { + restore_error_handler(); + } + } + + /** + * Returns a trimmed string. + * + * @param string|null $string + * @param string|null $characterMask + * @param string $side + * + * @return string + * + * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') + * + * @internal + */ + public static function trimFilter($string, $characterMask = null, $side = 'both') + { + if (null === $characterMask) { + $characterMask = " \t\n\r\0\x0B"; + } + + switch ($side) { + case 'both': + return trim($string ?? '', $characterMask); + case 'left': + return ltrim($string ?? '', $characterMask); + case 'right': + return rtrim($string ?? '', $characterMask); + default: + throw new RuntimeError('Trimming side must be "left", "right" or "both".'); + } + } + + /** + * Inserts HTML line breaks before all newlines in a string. + * + * @param string|null $string + * + * @return string + * + * @internal + */ + public static function nl2br($string) + { + return nl2br($string ?? ''); + } + + /** + * Removes whitespaces between HTML tags. + * + * @param string|null $string + * + * @return string + * + * @internal + */ + public static function spaceless($content) + { + return trim(preg_replace('/>\s+<', $content ?? '')); + } + + /** + * @param string|null $string + * @param string $to + * @param string $from + * + * @return string + * + * @internal + */ + public static function convertEncoding($string, $to, $from) + { + if (!\function_exists('iconv')) { + throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + } + + return iconv($from, $to, $string ?? ''); + } + + /** + * Returns the length of a variable. + * + * @param mixed $thing A variable + * + * @return int The length of the value + * + * @internal + */ + public static function lengthFilter(Environment $env, $thing) + { + if (null === $thing) { + return 0; + } + + if (\is_scalar($thing)) { + return mb_strlen($thing, $env->getCharset()); + } + + if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { + return \count($thing); + } + + if ($thing instanceof \Traversable) { + return iterator_count($thing); + } + + if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { + return mb_strlen((string) $thing, $env->getCharset()); + } + + return 1; + } + + /** + * Converts a string to uppercase. + * + * @param string|null $string A string + * + * @return string The uppercased string + * + * @internal + */ + public static function upperFilter(Environment $env, $string) + { + return mb_strtoupper($string ?? '', $env->getCharset()); + } + + /** + * Converts a string to lowercase. + * + * @param string|null $string A string + * + * @return string The lowercased string + * + * @internal + */ + public static function lowerFilter(Environment $env, $string) + { + return mb_strtolower($string ?? '', $env->getCharset()); + } + + /** + * Strips HTML and PHP tags from a string. + * + * @param string|null $string + * @param string[]|string|null $string + * + * @return string + * + * @internal + */ + public static function striptags($string, $allowable_tags = null) + { + return strip_tags($string ?? '', $allowable_tags); + } + + /** + * Returns a titlecased string. + * + * @param string|null $string A string + * + * @return string The titlecased string + * + * @internal + */ + public static function titleStringFilter(Environment $env, $string) + { + if (null !== $charset = $env->getCharset()) { + return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); + } + + return ucwords(strtolower($string ?? '')); + } + + /** + * Returns a capitalized string. + * + * @param string|null $string A string + * + * @return string The capitalized string + * + * @internal + */ + public static function capitalizeStringFilter(Environment $env, $string) + { + $charset = $env->getCharset(); + + return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); + } + + /** + * @internal + */ + public static function callMacro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) + { + if (!method_exists($template, $method)) { + $parent = $template; + while ($parent = $parent->getParent($context)) { + if (method_exists($parent, $method)) { + return $parent->$method(...$args); + } + } + + throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); + } + + return $template->$method(...$args); + } + + /** + * @internal + */ + public static function ensureTraversable($seq) + { + if (is_iterable($seq)) { + return $seq; + } + + return []; + } + + /** + * @internal + */ + public static function toArray($seq, $preserveKeys = true) + { + if ($seq instanceof \Traversable) { + return iterator_to_array($seq, $preserveKeys); + } + + if (!\is_array($seq)) { + return $seq; + } + + return $preserveKeys ? $seq : array_values($seq); + } + + /** + * Checks if a variable is empty. + * + * {# evaluates to true if the foo variable is null, false, or the empty string #} + * {% if foo is empty %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @return bool true if the value is empty, false otherwise + * + * @internal + */ + public static function testEmpty($value) + { + if ($value instanceof \Countable) { + return 0 === \count($value); + } + + if ($value instanceof \Traversable) { + return !iterator_count($value); + } + + if (\is_object($value) && method_exists($value, '__toString')) { + return '' === (string) $value; + } + + return '' === $value || false === $value || null === $value || [] === $value; + } + + /** + * Checks if a variable is traversable. + * + * {# evaluates to true if the foo variable is an array or a traversable object #} + * {% if foo is iterable %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @return bool true if the value is traversable + * + * @deprecated since Twig 3.8, to be removed in 4.0 (use the native "is_iterable" function instead) + * + * @internal + */ + public static function testIterable($value) + { + return is_iterable($value); + } + + /** + * Renders a template. + * + * @param array $context + * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively + * @param array $variables The variables to pass to the template + * @param bool $withContext + * @param bool $ignoreMissing Whether to ignore missing templates or not + * @param bool $sandboxed Whether to sandbox the template or not + * + * @return string The rendered template + * + * @internal + */ + public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) + { + $alreadySandboxed = false; + $sandbox = null; + if ($withContext) { + $variables = array_merge($context, $variables); + } + + if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { + $sandbox = $env->getExtension(SandboxExtension::class); + if (!$alreadySandboxed = $sandbox->isSandboxed()) { + $sandbox->enableSandbox(); + } + + foreach ((\is_array($template) ? $template : [$template]) as $name) { + // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security + if ($name instanceof TemplateWrapper || $name instanceof Template) { + $name->unwrap()->checkSecurity(); + } + } + } + + try { + $loaded = null; + try { + $loaded = $env->resolveTemplate($template); + } catch (LoaderError $e) { + if (!$ignoreMissing) { + throw $e; + } + } + + return $loaded ? $loaded->render($variables) : ''; + } finally { + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); + } + } + } + + /** + * Returns a template content without rendering it. + * + * @param string $name The template name + * @param bool $ignoreMissing Whether to ignore missing templates or not + * + * @return string The template source + * + * @internal + */ + public static function source(Environment $env, $name, $ignoreMissing = false) + { + $loader = $env->getLoader(); + try { + return $loader->getSourceContext($name)->getCode(); } catch (LoaderError $e) { if (!$ignoreMissing) { throw $e; } } - - return $loaded ? $loaded->render($variables) : ''; - } finally { - if ($isSandboxed && !$alreadySandboxed) { - $sandbox->disableSandbox(); - } - } -} - -/** - * Returns a template content without rendering it. - * - * @param string $name The template name - * @param bool $ignoreMissing Whether to ignore missing templates or not - * - * @return string The template source - */ -function twig_source(Environment $env, $name, $ignoreMissing = false) -{ - $loader = $env->getLoader(); - try { - return $loader->getSourceContext($name)->getCode(); - } catch (LoaderError $e) { - if (!$ignoreMissing) { - throw $e; - } - } -} - -/** - * Provides the ability to get constants from instances as well as class/global constants. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return string - */ -function twig_constant($constant, $object = null) -{ - if (null !== $object) { - if ('class' === $constant) { - return \get_class($object); - } - - $constant = \get_class($object).'::'.$constant; } - if (!\defined($constant)) { - throw new RuntimeError(sprintf('Constant "%s" is undefined.', $constant)); - } - - return \constant($constant); -} - -/** - * Checks if a constant exists. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return bool - */ -function twig_constant_is_defined($constant, $object = null) -{ - if (null !== $object) { - if ('class' === $constant) { - return true; - } - - $constant = \get_class($object).'::'.$constant; - } - - return \defined($constant); -} - -/** - * Batches item. - * - * @param array $items An array of items - * @param int $size The size of the batch - * @param mixed $fill A value used to fill missing items - * - * @return array - */ -function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) -{ - if (!is_iterable($items)) { - throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); - } - - $size = ceil($size); - - $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys); - - if (null !== $fill && $result) { - $last = \count($result) - 1; - if ($fillCount = $size - \count($result[$last])) { - for ($i = 0; $i < $fillCount; ++$i) { - $result[$last][] = $fill; + /** + * Provides the ability to get constants from instances as well as class/global constants. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * + * @return string + * + * @internal + */ + public static function constant($constant, $object = null) + { + if (null !== $object) { + if ('class' === $constant) { + return \get_class($object); } + + $constant = \get_class($object).'::'.$constant; } + + if (!\defined($constant)) { + throw new RuntimeError(sprintf('Constant "%s" is undefined.', $constant)); + } + + return \constant($constant); } - return $result; -} - -/** - * Returns the attribute value for a given array/object. - * - * @param mixed $object The object or array from where to get the item - * @param mixed $item The item to get from the array or object - * @param array $arguments An array of arguments to pass if the item is an object method - * @param string $type The type of attribute (@see \Twig\Template constants) - * @param bool $isDefinedTest Whether this is only a defined check - * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not - * @param int $lineno The template line where the attribute was called - * - * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true - * - * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false - * - * @internal - */ -function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) -{ - // array - if (/* Template::METHOD_CALL */ 'method' !== $type) { - $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; - - if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) - || ($object instanceof ArrayAccess && isset($object[$arrayItem])) - ) { - if ($isDefinedTest) { + /** + * Checks if a constant exists. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * + * @return bool + * + * @internal + */ + public static function constantIsDefined($constant, $object = null) + { + if (null !== $object) { + if ('class' === $constant) { return true; } - return $object[$arrayItem]; + $constant = \get_class($object).'::'.$constant; } - if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) { + return \defined($constant); + } + + /** + * Batches item. + * + * @param array $items An array of items + * @param int $size The size of the batch + * @param mixed $fill A value used to fill missing items + * + * @return array + * + * @internal + */ + public static function arrayBatch($items, $size, $fill = null, $preserveKeys = true) + { + if (!is_iterable($items)) { + throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); + } + + $size = ceil($size); + + $result = array_chunk(self::toArray($items, $preserveKeys), $size, $preserveKeys); + + if (null !== $fill && $result) { + $last = \count($result) - 1; + if ($fillCount = $size - \count($result[$last])) { + for ($i = 0; $i < $fillCount; ++$i) { + $result[$last][] = $fill; + } + } + } + + return $result; + } + + /** + * Returns the attribute value for a given array/object. + * + * @param mixed $object The object or array from where to get the item + * @param mixed $item The item to get from the array or object + * @param array $arguments An array of arguments to pass if the item is an object method + * @param string $type The type of attribute (@see \Twig\Template constants) + * @param bool $isDefinedTest Whether this is only a defined check + * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not + * @param int $lineno The template line where the attribute was called + * + * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true + * + * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false + * + * @internal + */ + public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) + { + // array + if (/* Template::METHOD_CALL */ 'method' !== $type) { + $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + + if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) + || ($object instanceof \ArrayAccess && isset($object[$arrayItem])) + ) { + if ($isDefinedTest) { + return true; + } + + return $object[$arrayItem]; + } + + if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + if ($object instanceof \ArrayAccess) { + $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); + } elseif (\is_object($object)) { + $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + } elseif (\is_array($object)) { + if (empty($object)) { + $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); + } else { + $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); + } + } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { + if (null === $object) { + $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); + } else { + $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + } + } elseif (null === $object) { + $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + } else { + $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + } + + throw new RuntimeError($message, $lineno, $source); + } + } + + if (!\is_object($object)) { if ($isDefinedTest) { return false; } @@ -1491,260 +1596,245 @@ function twig_get_attribute(Environment $env, Source $source, $object, $item, ar return; } - if ($object instanceof ArrayAccess) { - $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); - } elseif (\is_object($object)) { - $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + if (null === $object) { + $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); } elseif (\is_array($object)) { - if (empty($object)) { - $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); - } else { - $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); - } - } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { - if (null === $object) { - $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); - } else { - $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); - } - } elseif (null === $object) { - $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); } else { - $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); } throw new RuntimeError($message, $lineno, $source); } - } - if (!\is_object($object)) { - if ($isDefinedTest) { - return false; + if ($object instanceof Template) { + throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); } - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; - } + // object property + if (/* Template::METHOD_CALL */ 'method' !== $type) { + if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { + if ($isDefinedTest) { + return true; + } - if (null === $object) { - $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); - } elseif (\is_array($object)) { - $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); - } else { - $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); - } + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } - throw new RuntimeError($message, $lineno, $source); - } - - if ($object instanceof Template) { - throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); - } - - // object property - if (/* Template::METHOD_CALL */ 'method' !== $type) { - if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { - if ($isDefinedTest) { - return true; + return $object->$item; } - - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); - } - - return $object->$item; } - } - static $cache = []; + static $cache = []; - $class = \get_class($object); + $class = \get_class($object); - // object method - // precedence: getXxx() > isXxx() > hasXxx() - if (!isset($cache[$class])) { - $methods = get_class_methods($object); - sort($methods); - $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods); - $classCache = []; - foreach ($methods as $i => $method) { - $classCache[$method] = $method; - $classCache[$lcName = $lcMethods[$i]] = $method; + // object method + // precedence: getXxx() > isXxx() > hasXxx() + if (!isset($cache[$class])) { + $methods = get_class_methods($object); + sort($methods); + $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods); + $classCache = []; + foreach ($methods as $i => $method) { + $classCache[$method] = $method; + $classCache[$lcName = $lcMethods[$i]] = $method; - if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) { - $name = substr($method, 2); - $lcName = substr($lcName, 2); - } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - if (\in_array('is'.$lcName, $lcMethods)) { + if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) { + $name = substr($method, 2); + $lcName = substr($lcName, 2); + } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + if (\in_array('is'.$lcName, $lcMethods)) { + continue; + } + } else { continue; } - } else { - continue; - } - // skip get() and is() methods (in which case, $name is empty) - if ($name) { - if (!isset($classCache[$name])) { - $classCache[$name] = $method; - } + // skip get() and is() methods (in which case, $name is empty) + if ($name) { + if (!isset($classCache[$name])) { + $classCache[$name] = $method; + } - if (!isset($classCache[$lcName])) { - $classCache[$lcName] = $method; + if (!isset($classCache[$lcName])) { + $classCache[$lcName] = $method; + } } } + $cache[$class] = $classCache; + } + + $call = false; + if (isset($cache[$class][$item])) { + $method = $cache[$class][$item]; + } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { + $method = $cache[$class][$lcItem]; + } elseif (isset($cache[$class]['__call'])) { + $method = $item; + $call = true; + } else { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + 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); } - $cache[$class] = $classCache; - } - $call = false; - if (isset($cache[$class][$item])) { - $method = $cache[$class][$item]; - } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { - $method = $cache[$class][$lcItem]; - } elseif (isset($cache[$class]['__call'])) { - $method = $item; - $call = true; - } else { if ($isDefinedTest) { - return false; + return true; } - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); } - 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); + // Some objects throw exceptions when they have __call, and the method we try + // to call is not supported. If ignoreStrictCheck is true, we should return null. + try { + $ret = $object->$method(...$arguments); + } catch (\BadMethodCallException $e) { + if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { + return; + } + throw $e; + } + + return $ret; } - if ($isDefinedTest) { + /** + * Returns the values from a single column in the input array. + * + *
+     *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
+     *
+     *  {% set fruits = items|column('fruit') %}
+     *
+     *  {# fruits now contains ['apple', 'orange'] #}
+     * 
+ * + * @param array|Traversable $array An array + * @param mixed $name The column name + * @param mixed $index The column to use as the index/keys for the returned array + * + * @return array The array of values + * + * @internal + */ + public static function arrayColumn($array, $name, $index = null): array + { + if ($array instanceof \Traversable) { + $array = iterator_to_array($array); + } elseif (!\is_array($array)) { + throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + } + + return array_column($array, $name, $index); + } + + /** + * @internal + */ + public static function arrayFilter(Environment $env, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); + } + + self::checkArrowInSandbox($env, $arrow, 'filter', 'filter'); + + if (\is_array($array)) { + return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); + } + + // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator + return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); + } + + /** + * @internal + */ + public static function arrayMap(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'map', 'filter'); + + $r = []; + foreach ($array as $k => $v) { + $r[$k] = $arrow($v, $k); + } + + return $r; + } + + /** + * @internal + */ + public static function arrayReduce(Environment $env, $array, $arrow, $initial = null) + { + self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter'); + + if (!\is_array($array) && !$array instanceof \Traversable) { + throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + } + + $accumulator = $initial; + foreach ($array as $key => $value) { + $accumulator = $arrow($accumulator, $value, $key); + } + + return $accumulator; + } + + /** + * @internal + */ + public static function arraySome(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'has some', 'operator'); + + foreach ($array as $k => $v) { + if ($arrow($v, $k)) { + return true; + } + } + + return false; + } + + /** + * @internal + */ + public static function arrayEvery(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'has every', 'operator'); + + foreach ($array as $k => $v) { + if (!$arrow($v, $k)) { + return false; + } + } + return true; } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); - } - - // Some objects throw exceptions when they have __call, and the method we try - // to call is not supported. If ignoreStrictCheck is true, we should return null. - try { - $ret = $object->$method(...$arguments); - } catch (\BadMethodCallException $e) { - if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { - return; - } - throw $e; - } - - return $ret; -} - -/** - * Returns the values from a single column in the input array. - * - *
- *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
- *
- *  {% set fruits = items|column('fruit') %}
- *
- *  {# fruits now contains ['apple', 'orange'] #}
- * 
- * - * @param array|Traversable $array An array - * @param mixed $name The column name - * @param mixed $index The column to use as the index/keys for the returned array - * - * @return array The array of values - */ -function twig_array_column($array, $name, $index = null): array -{ - if ($array instanceof Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); - } - - return array_column($array, $name, $index); -} - -function twig_array_filter(Environment $env, $array, $arrow) -{ - if (!is_iterable($array)) { - throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); - } - - twig_check_arrow_in_sandbox($env, $arrow, 'filter', 'filter'); - - if (\is_array($array)) { - return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); - } - - // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator - return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); -} - -function twig_array_map(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'map', 'filter'); - - $r = []; - foreach ($array as $k => $v) { - $r[$k] = $arrow($v, $k); - } - - return $r; -} - -function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'reduce', 'filter'); - - if (!\is_array($array) && !$array instanceof \Traversable) { - throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); - } - - $accumulator = $initial; - foreach ($array as $key => $value) { - $accumulator = $arrow($accumulator, $value, $key); - } - - return $accumulator; -} - -function twig_array_some(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'has some', 'operator'); - - foreach ($array as $k => $v) { - if ($arrow($v, $k)) { - return true; + /** + * @internal + */ + public static function checkArrowInSandbox(Environment $env, $arrow, $thing, $type) + { + if (!$arrow instanceof \Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { + throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); } } - - return false; -} - -function twig_array_every(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'has every', 'operator'); - - foreach ($array as $k => $v) { - if (!$arrow($v, $k)) { - return false; - } - } - - return true; -} - -function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) -{ - if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { - throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); - } -} } diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 444283802..075c13590 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -70,7 +70,7 @@ class ArrayExpression extends AbstractExpression $needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs); if ($needsArrayMergeSpread) { - $compiler->raw('twig_array_merge('); + $compiler->raw('CoreExtension::arrayMerge('); } $compiler->raw('['); $first = true; diff --git a/src/Node/Expression/Binary/EqualBinary.php b/src/Node/Expression/Binary/EqualBinary.php index 6b48549ef..5f423196f 100644 --- a/src/Node/Expression/Binary/EqualBinary.php +++ b/src/Node/Expression/Binary/EqualBinary.php @@ -24,7 +24,7 @@ class EqualBinary extends AbstractBinary } $compiler - ->raw('(0 === twig_compare(') + ->raw('(0 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/GreaterBinary.php b/src/Node/Expression/Binary/GreaterBinary.php index e1dd06780..f42de3f86 100644 --- a/src/Node/Expression/Binary/GreaterBinary.php +++ b/src/Node/Expression/Binary/GreaterBinary.php @@ -24,7 +24,7 @@ class GreaterBinary extends AbstractBinary } $compiler - ->raw('(1 === twig_compare(') + ->raw('(1 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/GreaterEqualBinary.php b/src/Node/Expression/Binary/GreaterEqualBinary.php index df9bfcfbf..0c4f43fd9 100644 --- a/src/Node/Expression/Binary/GreaterEqualBinary.php +++ b/src/Node/Expression/Binary/GreaterEqualBinary.php @@ -24,7 +24,7 @@ class GreaterEqualBinary extends AbstractBinary } $compiler - ->raw('(0 <= twig_compare(') + ->raw('(0 <= CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/HasEveryBinary.php b/src/Node/Expression/Binary/HasEveryBinary.php index adfabd44c..c57bb20e9 100644 --- a/src/Node/Expression/Binary/HasEveryBinary.php +++ b/src/Node/Expression/Binary/HasEveryBinary.php @@ -18,7 +18,7 @@ class HasEveryBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_array_every($this->env, ') + ->raw('CoreExtension::arrayEvery($this->env, ') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/HasSomeBinary.php b/src/Node/Expression/Binary/HasSomeBinary.php index 270da3692..12293f84c 100644 --- a/src/Node/Expression/Binary/HasSomeBinary.php +++ b/src/Node/Expression/Binary/HasSomeBinary.php @@ -18,7 +18,7 @@ class HasSomeBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_array_some($this->env, ') + ->raw('CoreExtension::arraySome($this->env, ') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/InBinary.php b/src/Node/Expression/Binary/InBinary.php index 6dbfa97f0..68a98fe15 100644 --- a/src/Node/Expression/Binary/InBinary.php +++ b/src/Node/Expression/Binary/InBinary.php @@ -18,7 +18,7 @@ class InBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_in_filter(') + ->raw('CoreExtension::inFilter(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/LessBinary.php b/src/Node/Expression/Binary/LessBinary.php index 598e62913..fb3264a2d 100644 --- a/src/Node/Expression/Binary/LessBinary.php +++ b/src/Node/Expression/Binary/LessBinary.php @@ -24,7 +24,7 @@ class LessBinary extends AbstractBinary } $compiler - ->raw('(-1 === twig_compare(') + ->raw('(-1 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/LessEqualBinary.php b/src/Node/Expression/Binary/LessEqualBinary.php index e3c4af58d..8f3653892 100644 --- a/src/Node/Expression/Binary/LessEqualBinary.php +++ b/src/Node/Expression/Binary/LessEqualBinary.php @@ -24,7 +24,7 @@ class LessEqualBinary extends AbstractBinary } $compiler - ->raw('(0 >= twig_compare(') + ->raw('(0 >= CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/MatchesBinary.php b/src/Node/Expression/Binary/MatchesBinary.php index a8bce6f4e..4669044e0 100644 --- a/src/Node/Expression/Binary/MatchesBinary.php +++ b/src/Node/Expression/Binary/MatchesBinary.php @@ -18,7 +18,7 @@ class MatchesBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_matches(') + ->raw('CoreExtension::matches(') ->subcompile($this->getNode('right')) ->raw(', ') ->subcompile($this->getNode('left')) diff --git a/src/Node/Expression/Binary/NotEqualBinary.php b/src/Node/Expression/Binary/NotEqualBinary.php index db47a2890..d137ef627 100644 --- a/src/Node/Expression/Binary/NotEqualBinary.php +++ b/src/Node/Expression/Binary/NotEqualBinary.php @@ -24,7 +24,7 @@ class NotEqualBinary extends AbstractBinary } $compiler - ->raw('(0 !== twig_compare(') + ->raw('(0 !== CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/NotInBinary.php b/src/Node/Expression/Binary/NotInBinary.php index fcba6cca1..80c8755d8 100644 --- a/src/Node/Expression/Binary/NotInBinary.php +++ b/src/Node/Expression/Binary/NotInBinary.php @@ -18,7 +18,7 @@ class NotInBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('!twig_in_filter(') + ->raw('!CoreExtension::inFilter(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/FunctionExpression.php b/src/Node/Expression/FunctionExpression.php index 71269775c..e89b38978 100644 --- a/src/Node/Expression/FunctionExpression.php +++ b/src/Node/Expression/FunctionExpression.php @@ -12,6 +12,7 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Extension\CoreExtension; use Twig\Node\Node; class FunctionExpression extends CallExpression @@ -33,7 +34,7 @@ class FunctionExpression extends CallExpression $this->setAttribute('arguments', $function->getArguments()); $callable = $function->getCallable(); if ('constant' === $name && $this->getAttribute('is_defined_test')) { - $callable = 'twig_constant_is_defined'; + $callable = [CoreExtension::class, 'constantIsDefined']; } $this->setAttribute('callable', $callable); $this->setAttribute('is_variadic', $function->isVariadic()); diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index e6a75ce94..29a446b88 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -57,7 +57,7 @@ class GetAttrExpression extends AbstractExpression return; } - $compiler->raw('twig_get_attribute($this->env, $this->source, '); + $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); if ($this->getAttribute('ignore_strict_check')) { $this->getNode('node')->setAttribute('ignore_strict_check', true); diff --git a/src/Node/Expression/MethodCallExpression.php b/src/Node/Expression/MethodCallExpression.php index d5ec0b6ef..6fa1c3f9e 100644 --- a/src/Node/Expression/MethodCallExpression.php +++ b/src/Node/Expression/MethodCallExpression.php @@ -39,7 +39,7 @@ class MethodCallExpression extends AbstractExpression } $compiler - ->raw('twig_call_macro($macros[') + ->raw('CoreExtension::callMacro($macros[') ->repr($this->getNode('node')->getAttribute('name')) ->raw('], ') ->repr($this->getAttribute('method')) diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index 04addfbfe..78b361d8a 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -42,7 +42,7 @@ class ForNode extends Node $compiler ->addDebugInfo($this) ->write("\$context['_parent'] = \$context;\n") - ->write("\$context['_seq'] = twig_ensure_traversable(") + ->write("\$context['_seq'] = CoreExtension::ensureTraversable(") ->subcompile($this->getNode('seq')) ->raw(");\n") ; diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index d540d6b23..be36b2657 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -93,12 +93,12 @@ class IncludeNode extends Node implements NodeOutputInterface $compiler->raw(false === $this->getAttribute('only') ? '$context' : '[]'); } elseif (false === $this->getAttribute('only')) { $compiler - ->raw('twig_array_merge($context, ') + ->raw('CoreExtension::arrayMerge($context, ') ->subcompile($this->getNode('variables')) ->raw(')') ; } else { - $compiler->raw('twig_to_array('); + $compiler->raw('CoreExtension::toArray('); $compiler->subcompile($this->getNode('variables')); $compiler->raw(')'); } diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 9b485eeaf..dce335c63 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -143,6 +143,7 @@ final class ModuleNode extends Node ->write("use Twig\Environment;\n") ->write("use Twig\Error\LoaderError;\n") ->write("use Twig\Error\RuntimeError;\n") + ->write("use Twig\Extension\CoreExtension;\n") ->write("use Twig\Extension\SandboxExtension;\n") ->write("use Twig\Markup;\n") ->write("use Twig\Sandbox\SecurityError;\n") diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index 2ac9123d0..302b40389 100644 --- a/src/Node/WithNode.php +++ b/src/Node/WithNode.php @@ -52,7 +52,7 @@ class WithNode extends Node ->raw(", \$this->getSourceContext());\n") ->outdent() ->write("}\n") - ->write(sprintf("\$%s = twig_to_array(\$%s);\n", $varsName, $varsName)) + ->write(sprintf("\$%s = CoreExtension::toArray(\$%s);\n", $varsName, $varsName)) ; if ($this->getAttribute('only')) { diff --git a/src/Resources/core.php b/src/Resources/core.php new file mode 100644 index 000000000..f58a1fdf2 --- /dev/null +++ b/src/Resources/core.php @@ -0,0 +1,497 @@ +env, $this->source, '; + return 'CoreExtension::getAttribute($this->env, $this->source, '; } } diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 431517f6b..e1971aeea 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -14,6 +14,7 @@ namespace Twig\Tests\Extension; use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Loader\LoaderInterface; class CoreTest extends TestCase @@ -26,7 +27,7 @@ class CoreTest extends TestCase $env = new Environment($this->createMock(LoaderInterface::class)); for ($i = 0; $i < 100; ++$i) { - $this->assertTrue(\in_array(twig_random($env, $value1, $value2), $expectedInArray, true)); // assertContains() would not consider the type + $this->assertTrue(\in_array(CoreExtension::random($env, $value1, $value2), $expectedInArray, true)); // assertContains() would not consider the type } } @@ -84,24 +85,24 @@ class CoreTest extends TestCase $max = mt_getrandmax(); for ($i = 0; $i < 100; ++$i) { - $val = twig_random(new Environment($this->createMock(LoaderInterface::class))); + $val = CoreExtension::random(new Environment($this->createMock(LoaderInterface::class))); $this->assertTrue(\is_int($val) && $val >= 0 && $val <= $max); } } public function testRandomFunctionReturnsAsIs() { - $this->assertSame('', twig_random(new Environment($this->createMock(LoaderInterface::class)), '')); - $this->assertSame('', twig_random(new Environment($this->createMock(LoaderInterface::class), ['charset' => null]), '')); + $this->assertSame('', CoreExtension::random(new Environment($this->createMock(LoaderInterface::class)), '')); + $this->assertSame('', CoreExtension::random(new Environment($this->createMock(LoaderInterface::class), ['charset' => null]), '')); $instance = new \stdClass(); - $this->assertSame($instance, twig_random(new Environment($this->createMock(LoaderInterface::class)), $instance)); + $this->assertSame($instance, CoreExtension::random(new Environment($this->createMock(LoaderInterface::class)), $instance)); } public function testRandomFunctionOfEmptyArrayThrowsException() { $this->expectException(RuntimeError::class); - twig_random(new Environment($this->createMock(LoaderInterface::class)), []); + CoreExtension::random(new Environment($this->createMock(LoaderInterface::class)), []); } public function testRandomFunctionOnNonUTF8String() @@ -111,7 +112,7 @@ class CoreTest extends TestCase $text = iconv('UTF-8', 'ISO-8859-1', 'Äé'); for ($i = 0; $i < 30; ++$i) { - $rand = twig_random($twig, $text); + $rand = CoreExtension::random($twig, $text); $this->assertTrue(\in_array(iconv('ISO-8859-1', 'UTF-8', $rand), ['Ä', 'é'], true)); } } @@ -122,7 +123,7 @@ class CoreTest extends TestCase $twig->setCharset('ISO-8859-1'); $input = iconv('UTF-8', 'ISO-8859-1', 'Äé'); - $output = iconv('ISO-8859-1', 'UTF-8', twig_reverse_filter($twig, $input)); + $output = iconv('ISO-8859-1', 'UTF-8', CoreExtension::reverseFilter($twig, $input)); $this->assertEquals($output, 'éÄ'); } @@ -133,7 +134,7 @@ class CoreTest extends TestCase public function testTwigFirst($expected, $input) { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertSame($expected, twig_first($twig, $input)); + $this->assertSame($expected, CoreExtension::first($twig, $input)); } public function provideTwigFirstCases() @@ -155,7 +156,7 @@ class CoreTest extends TestCase public function testTwigLast($expected, $input) { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertSame($expected, twig_last($twig, $input)); + $this->assertSame($expected, CoreExtension::last($twig, $input)); } public function provideTwigLastCases() @@ -176,7 +177,7 @@ class CoreTest extends TestCase */ public function testArrayKeysFilter(array $expected, $input) { - $this->assertSame($expected, twig_get_array_keys_filter($input)); + $this->assertSame($expected, CoreExtension::getArrayKeysFilter($input)); } public function provideArrayKeyCases() @@ -199,7 +200,7 @@ class CoreTest extends TestCase */ public function testInFilter($expected, $value, $compare) { - $this->assertSame($expected, twig_in_filter($value, $compare)); + $this->assertSame($expected, CoreExtension::inFilter($value, $compare)); } public function provideInFilterCases() @@ -228,7 +229,7 @@ class CoreTest extends TestCase public function testSliceFilter($expected, $input, $start, $length = null, $preserveKeys = false) { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertSame($expected, twig_slice($twig, $input, $start, $length, $preserveKeys)); + $this->assertSame($expected, CoreExtension::slice($twig, $input, $start, $length, $preserveKeys)); } public function provideSliceFilterCases() @@ -257,16 +258,16 @@ class CoreTest extends TestCase */ public function testCompare($expected, $a, $b) { - $this->assertSame($expected, twig_compare($a, $b)); - $this->assertSame($expected, -twig_compare($b, $a)); + $this->assertSame($expected, CoreExtension::compare($a, $b)); + $this->assertSame($expected, -CoreExtension::compare($b, $a)); } public function testCompareNAN() { - $this->assertSame(1, twig_compare(\NAN, 'NAN')); - $this->assertSame(1, twig_compare('NAN', \NAN)); - $this->assertSame(1, twig_compare(\NAN, 'foo')); - $this->assertSame(1, twig_compare('foo', \NAN)); + $this->assertSame(1, CoreExtension::compare(\NAN, 'NAN')); + $this->assertSame(1, CoreExtension::compare('NAN', \NAN)); + $this->assertSame(1, CoreExtension::compare(\NAN, 'foo')); + $this->assertSame(1, CoreExtension::compare('foo', \NAN)); } public function provideCompareCases() diff --git a/tests/Extension/StringLoaderExtensionTest.php b/tests/Extension/StringLoaderExtensionTest.php index e4fa2e3ba..4c67e12c7 100644 --- a/tests/Extension/StringLoaderExtensionTest.php +++ b/tests/Extension/StringLoaderExtensionTest.php @@ -13,6 +13,7 @@ namespace Twig\Tests\Extension; use PHPUnit\Framework\TestCase; use Twig\Environment; +use Twig\Extension\CoreExtension; use Twig\Extension\StringLoaderExtension; class StringLoaderExtensionTest extends TestCase @@ -21,6 +22,6 @@ class StringLoaderExtensionTest extends TestCase { $twig = new Environment($this->createMock('\Twig\Loader\LoaderInterface')); $twig->addExtension(new StringLoaderExtension()); - $this->assertSame('something', twig_include($twig, [], StringLoaderExtension::templateFromString($twig, 'something'))); + $this->assertSame('something', CoreExtension::include($twig, [], StringLoaderExtension::templateFromString($twig, 'something'))); } } diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index b8cc48fab..18820cac8 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -69,7 +69,7 @@ class FilterTest extends NodeTestCase $node = $this->createFilter($expr, 'upper'); $node = $this->createFilter($node, 'number_format', [new ConstantExpression(2, 1), new ConstantExpression('.', 1), new ConstantExpression(',', 1)]); - $tests[] = [$node, 'twig_number_format_filter($this->env, twig_upper_filter($this->env, "foo"), 2, ".", ",")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::numberFormatFilter($this->env, Twig\Extension\CoreExtension::upperFilter($this->env, "foo"), 2, ".", ",")']; // named arguments $date = new ConstantExpression(0, 1); @@ -77,25 +77,25 @@ class FilterTest extends NodeTestCase 'timezone' => new ConstantExpression('America/Chicago', 1), 'format' => new ConstantExpression('d/m/Y H:i:s P', 1), ]); - $tests[] = [$node, 'twig_date_format_filter($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::dateFormatFilter($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")']; // skip an optional argument $date = new ConstantExpression(0, 1); $node = $this->createFilter($date, 'date', [ 'timezone' => new ConstantExpression('America/Chicago', 1), ]); - $tests[] = [$node, 'twig_date_format_filter($this->env, 0, null, "America/Chicago")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::dateFormatFilter($this->env, 0, null, "America/Chicago")']; // underscores vs camelCase for named arguments $string = new ConstantExpression('abc', 1); $node = $this->createFilter($string, 'reverse', [ 'preserve_keys' => new ConstantExpression(true, 1), ]); - $tests[] = [$node, 'twig_reverse_filter($this->env, "abc", true)']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::reverseFilter($this->env, "abc", true)']; $node = $this->createFilter($string, 'reverse', [ 'preserveKeys' => new ConstantExpression(true, 1), ]); - $tests[] = [$node, 'twig_reverse_filter($this->env, "abc", true)']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::reverseFilter($this->env, "abc", true)']; // filter as an anonymous function $node = $this->createFilter(new ConstantExpression('foo', 1), 'anonymous'); diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php index 05c07c029..7fde21649 100644 --- a/tests/Node/Expression/FunctionTest.php +++ b/tests/Node/Expression/FunctionTest.php @@ -76,7 +76,7 @@ class FunctionTest extends NodeTestCase 'timezone' => new ConstantExpression('America/Chicago', 1), 'date' => new ConstantExpression(0, 1), ]); - $tests[] = [$node, 'twig_date_converter($this->env, 0, "America/Chicago")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::dateConverter($this->env, 0, "America/Chicago")']; // arbitrary named arguments $node = $this->createFunction('barbar'); diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index ce7464817..dbfac3269 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -57,7 +57,7 @@ class ForTest extends NodeTestCase $tests[] = [$node, <<getVariableGetter('items')}); +\$context['_seq'] = CoreExtension::ensureTraversable({$this->getVariableGetter('items')}); foreach (\$context['_seq'] as \$context["key"] => \$context["item"]) { echo {$this->getVariableGetter('foo')}; } @@ -78,7 +78,7 @@ EOF $tests[] = [$node, <<getVariableGetter('values')}); +\$context['_seq'] = CoreExtension::ensureTraversable({$this->getVariableGetter('values')}); \$context['loop'] = [ 'parent' => \$context['_parent'], 'index0' => 0, @@ -120,7 +120,7 @@ EOF $tests[] = [$node, <<getVariableGetter('values')}); +\$context['_seq'] = CoreExtension::ensureTraversable({$this->getVariableGetter('values')}); \$context['loop'] = [ 'parent' => \$context['_parent'], 'index0' => 0, @@ -162,7 +162,7 @@ EOF $tests[] = [$node, <<getVariableGetter('values')}); +\$context['_seq'] = CoreExtension::ensureTraversable({$this->getVariableGetter('values')}); \$context['_iterated'] = false; \$context['loop'] = [ 'parent' => \$context['_parent'], diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index 92681662d..6d96373bf 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -64,14 +64,14 @@ EOF $node = new IncludeNode($expr, $vars, false, false, 1); $tests[] = [$node, <<loadTemplate("foo.twig", null, 1)->display(twig_array_merge(\$context, ["foo" => true])); +\$this->loadTemplate("foo.twig", null, 1)->display(CoreExtension::arrayMerge(\$context, ["foo" => true])); EOF ]; $node = new IncludeNode($expr, $vars, true, false, 1); $tests[] = [$node, <<loadTemplate("foo.twig", null, 1)->display(twig_to_array(["foo" => true])); +\$this->loadTemplate("foo.twig", null, 1)->display(CoreExtension::toArray(["foo" => true])); EOF ]; @@ -85,7 +85,7 @@ try { // ignore missing template } if (\$__internal_%s) { - \$__internal_%s->display(twig_to_array(["foo" => true])); + \$__internal_%s->display(CoreExtension::toArray(["foo" => true])); } EOF , null, true]; diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 938fa2ea7..d6b378ad5 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -63,6 +63,7 @@ class ModuleTest extends NodeTestCase use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Markup; use Twig\Sandbox\SecurityError; @@ -110,7 +111,7 @@ class __TwigTemplate_%x extends Template */ public function getDebugInfo() { - return array ( 37 => 1,); + return array ( 38 => 1,); } public function getSourceContext() @@ -133,6 +134,7 @@ EOF use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Markup; use Twig\Sandbox\SecurityError; @@ -195,7 +197,7 @@ class __TwigTemplate_%x extends Template */ public function getDebugInfo() { - return array ( 43 => 1, 41 => 2, 34 => 1,); + return array ( 44 => 1, 42 => 2, 35 => 1,); } public function getSourceContext() @@ -223,6 +225,7 @@ EOF use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Markup; use Twig\Sandbox\SecurityError; @@ -284,7 +287,7 @@ class __TwigTemplate_%x extends Template */ public function getDebugInfo() { - return array ( 43 => 2, 41 => 4, 34 => 2,); + return array ( 44 => 2, 42 => 4, 35 => 2,); } public function getSourceContext() diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 9c2364c1d..b14a0384a 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -14,6 +14,7 @@ namespace Twig\Tests; use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Loader\ArrayLoader; use Twig\Loader\LoaderInterface; @@ -94,7 +95,7 @@ class TemplateTest extends TestCase $template = new TemplateForTest($twig); try { - twig_get_attribute($twig, $template->getSourceContext(), $object, $item, [], 'any', false, false, true); + CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, [], 'any', false, false, true); if (!$allowed) { $this->fail(); @@ -180,14 +181,14 @@ class TemplateTest extends TestCase $this->assertSame('IntegerButStringWithLeadingZeros', $array['01']); $this->assertSame('EmptyString', $array[null]); - $this->assertSame('Zero', twig_get_attribute($twig, $template->getSourceContext(), $array, false), 'false is treated as 0 when accessing an array (equals PHP behavior)'); - $this->assertSame('One', twig_get_attribute($twig, $template->getSourceContext(), $array, true), 'true is treated as 1 when accessing an array (equals PHP behavior)'); - $this->assertSame('One', twig_get_attribute($twig, $template->getSourceContext(), $array, 1.5), 'float is casted to int when accessing an array (equals PHP behavior)'); - $this->assertSame('One', twig_get_attribute($twig, $template->getSourceContext(), $array, '1'), '"1" is treated as integer 1 when accessing an array (equals PHP behavior)'); - $this->assertSame('MinusOne', twig_get_attribute($twig, $template->getSourceContext(), $array, -1.5), 'negative float is casted to int when accessing an array (equals PHP behavior)'); - $this->assertSame('FloatButString', twig_get_attribute($twig, $template->getSourceContext(), $array, '1.5'), '"1.5" is treated as-is when accessing an array (equals PHP behavior)'); - $this->assertSame('IntegerButStringWithLeadingZeros', twig_get_attribute($twig, $template->getSourceContext(), $array, '01'), '"01" is treated as-is when accessing an array (equals PHP behavior)'); - $this->assertSame('EmptyString', twig_get_attribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing an array (equals PHP behavior)'); + $this->assertSame('Zero', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, false), 'false is treated as 0 when accessing an array (equals PHP behavior)'); + $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, true), 'true is treated as 1 when accessing an array (equals PHP behavior)'); + $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, 1.5), 'float is casted to int when accessing an array (equals PHP behavior)'); + $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1'), '"1" is treated as integer 1 when accessing an array (equals PHP behavior)'); + $this->assertSame('MinusOne', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, -1.5), 'negative float is casted to int when accessing an array (equals PHP behavior)'); + $this->assertSame('FloatButString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1.5'), '"1.5" is treated as-is when accessing an array (equals PHP behavior)'); + $this->assertSame('IntegerButStringWithLeadingZeros', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '01'), '"01" is treated as-is when accessing an array (equals PHP behavior)'); + $this->assertSame('EmptyString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing an array (equals PHP behavior)'); } /** @@ -198,7 +199,7 @@ class TemplateTest extends TestCase $twig = new Environment($this->createMock(LoaderInterface::class)); $template = new TemplateForTest($twig); - $this->assertEquals($value, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); + $this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); } /** @@ -210,13 +211,13 @@ class TemplateTest extends TestCase $template = new TemplateForTest($twig); if ($defined) { - $this->assertEquals($value, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); + $this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); } else { $this->expectException(RuntimeError::class); if (null !== $exceptionMessage) { $this->expectExceptionMessage($exceptionMessage); } - $this->assertEquals($value, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); + $this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); } } @@ -228,7 +229,7 @@ class TemplateTest extends TestCase $twig = new Environment($this->createMock(LoaderInterface::class)); $template = new TemplateForTest($twig); - $this->assertEquals($defined, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); + $this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); } /** @@ -239,7 +240,7 @@ class TemplateTest extends TestCase $twig = new Environment($this->createMock(LoaderInterface::class), ['strict_variables' => true]); $template = new TemplateForTest($twig); - $this->assertEquals($defined, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); + $this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); } public function testGetAttributeCallExceptions() @@ -249,7 +250,7 @@ class TemplateTest extends TestCase $object = new TemplateMagicMethodExceptionObject(); - $this->assertNull(twig_get_attribute($twig, $template->getSourceContext(), $object, 'foo')); + $this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, 'foo')); } public function getGetAttributeTests() @@ -395,9 +396,9 @@ class TemplateTest extends TestCase $getIsObject = new TemplateGetIsMethods(); $template = new TemplateForTest($twig, 'index.twig'); // first time should not create a cache for "get" - $this->assertNull(twig_get_attribute($twig, $template->getSourceContext(), $getIsObject, 'get')); + $this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $getIsObject, 'get')); // 0 should be in the method cache now, so this should fail - $this->assertNull(twig_get_attribute($twig, $template->getSourceContext(), $getIsObject, 0)); + $this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $getIsObject, 0)); } }