From 92bc110bcfc435b41376c72a19ec5ef9eec4130e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 2 Jan 2022 13:59:35 +0100 Subject: [PATCH] Allow null when Twig expects a string --- CHANGELOG | 4 +- src/Extension/CoreExtension.php | 79 ++++++++++++-------- tests/Fixtures/filters/capitalize.test | 14 ++++ tests/Fixtures/filters/convert_encoding.test | 4 + tests/Fixtures/filters/escape.test | 4 + tests/Fixtures/filters/first.test | 5 +- tests/Fixtures/filters/last.test | 5 +- tests/Fixtures/filters/length.test | 2 + tests/Fixtures/filters/lower.test | 12 +++ tests/Fixtures/filters/number_format.test | 6 ++ tests/Fixtures/filters/replace.test | 4 + tests/Fixtures/filters/reverse.test | 4 + tests/Fixtures/filters/round.test | 8 ++ tests/Fixtures/filters/spaceless.test | 4 + tests/Fixtures/filters/split.test | 6 +- tests/Fixtures/filters/title.test | 12 +++ tests/Fixtures/filters/trim.test | 12 +++ tests/Fixtures/filters/upper.test | 12 +++ tests/Fixtures/filters/urlencode.test | 4 + 19 files changed, 164 insertions(+), 37 deletions(-) create mode 100644 tests/Fixtures/filters/capitalize.test create mode 100644 tests/Fixtures/filters/lower.test create mode 100644 tests/Fixtures/filters/title.test create mode 100644 tests/Fixtures/filters/upper.test diff --git a/CHANGELOG b/CHANGELOG index a5f36d6e9..5ebd566f7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ -# 2.14.9 (2021-XX-XX) +# 2.14.9 (2022-XX-XX) -* n/a +* Allow null when Twig expects a string (for better 8.1 support) # 2.14.8 (2021-11-25) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 883ff4d68..0f77c52b0 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -543,7 +543,7 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null) /** * Replaces strings within a string. * - * @param string $str String to replace in + * @param string|null $str String to replace in * @param array|\Traversable $from Replace values * * @return string @@ -554,15 +554,15 @@ function twig_replace_filter($str, $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)); + return strtr($str ?? '', twig_to_array($from)); } /** * Rounds a number. * - * @param int|float $value The value to round - * @param int|float $precision The rounding precision - * @param string $method The method to use for rounding + * @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 */ @@ -576,6 +576,8 @@ function twig_round($value, $precision = 0, $method = 'common') throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); } + $value = (float) $value; + return $method($value * 10 ** $precision) / 10 ** $precision; } @@ -583,7 +585,7 @@ function twig_round($value, $precision = 0, $method = 'common') * 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 + * 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 @@ -614,7 +616,7 @@ function twig_number_format_filter(Environment $env, $number, $decimal = null, $ /** * URL encodes (RFC 3986) a string as a path segment or an array as a query string. * - * @param string|array $url A URL or an array of query parameters + * @param string|array|null $url A URL or an array of query parameters * * @return string The URL encoded value */ @@ -624,7 +626,7 @@ function twig_urlencode_filter($url) return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); } - return rawurlencode($url); + return rawurlencode($url ?? ''); } /** @@ -686,9 +688,7 @@ function twig_slice(Environment $env, $item, $start, $length = null, $preserveKe return \array_slice($item, $start, $length, $preserveKeys); } - $item = (string) $item; - - return (string) mb_substr($item, $start, $length, $env->getCharset()); + return (string) mb_substr((string) $item, $start, $length, $env->getCharset()); } /** @@ -777,14 +777,16 @@ function twig_join_filter($value, $glue = '', $and = null) * {{ "aabbcc"|split('', 2) }} * {# returns [aa, bb, cc] #} * - * @param string $value A string - * @param string $delimiter The delimiter - * @param int $limit The limit + * @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 (\strlen($delimiter) > 0) { return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); } @@ -870,8 +872,8 @@ function twig_get_array_keys_filter($array) /** * Reverses a variable. * - * @param array|\Traversable|string $item An array, a \Traversable instance, or a string - * @param bool $preserveKeys Whether to preserve key or not + * @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 */ @@ -890,10 +892,10 @@ function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) $charset = $env->getCharset(); if ('UTF-8' !== $charset) { - $item = twig_convert_encoding($string, 'UTF-8', $charset); + $string = twig_convert_encoding($string, 'UTF-8', $charset); } - preg_match_all('/./us', $item, $matches); + preg_match_all('/./us', $string, $matches); $string = implode('', array_reverse($matches[0])); @@ -968,6 +970,10 @@ function twig_in_filter($value, $compare) /** * 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') @@ -980,11 +986,11 @@ function twig_trim_filter($string, $characterMask = null, $side = 'both') switch ($side) { case 'both': - return trim($string, $characterMask); + return trim($string ?? '', $characterMask); case 'left': - return ltrim($string, $characterMask); + return ltrim($string ?? '', $characterMask); case 'right': - return rtrim($string, $characterMask); + return rtrim($string ?? '', $characterMask); default: throw new RuntimeError('Trimming side must be "left", "right" or "both".'); } @@ -993,20 +999,29 @@ function twig_trim_filter($string, $characterMask = null, $side = 'both') /** * Removes whitespaces between HTML tags. * + * @param string|null $string + * * @return string */ function twig_spaceless($content) { - return trim(preg_replace('/>\s+<', $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); + return iconv($from, $to, $string ?? ''); } /** @@ -1044,47 +1059,47 @@ function twig_length_filter(Environment $env, $thing) /** * Converts a string to uppercase. * - * @param string $string A string + * @param string|null $string A string * * @return string The uppercased string */ function twig_upper_filter(Environment $env, $string) { - return mb_strtoupper($string, $env->getCharset()); + return mb_strtoupper($string ?? '', $env->getCharset()); } /** * Converts a string to lowercase. * - * @param string $string A string + * @param string|null $string A string * * @return string The lowercased string */ function twig_lower_filter(Environment $env, $string) { - return mb_strtolower($string, $env->getCharset()); + return mb_strtolower($string ?? '', $env->getCharset()); } /** * Returns a titlecased string. * - * @param string $string A 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 mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); } - return ucwords(strtolower($string)); + return ucwords(strtolower($string ?? '')); } /** * Returns a capitalized string. * - * @param string $string A string + * @param string|null $string A string * * @return string The capitalized string */ @@ -1092,7 +1107,7 @@ 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); + return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); } /** diff --git a/tests/Fixtures/filters/capitalize.test b/tests/Fixtures/filters/capitalize.test new file mode 100644 index 000000000..bea82ba7b --- /dev/null +++ b/tests/Fixtures/filters/capitalize.test @@ -0,0 +1,14 @@ +--TEST-- +"capitalize" filter +--TEMPLATE-- +{{ "super helpful"|capitalize }} +{{ "a"|capitalize }} +*{{ ""|capitalize }}* +*{{ null|capitalize }}* +--DATA-- +return [] +--EXPECT-- +Super helpful +A +** +** diff --git a/tests/Fixtures/filters/convert_encoding.test b/tests/Fixtures/filters/convert_encoding.test index 6f0091f30..a2a324855 100644 --- a/tests/Fixtures/filters/convert_encoding.test +++ b/tests/Fixtures/filters/convert_encoding.test @@ -2,7 +2,11 @@ "convert_encoding" filter --TEMPLATE-- {{ "愛していますか?"|convert_encoding('ISO-2022-JP', 'UTF-8')|convert_encoding('UTF-8', 'ISO-2022-JP') }} +*{{ ""|convert_encoding('ISO-2022-JP', 'UTF-8')|convert_encoding('UTF-8', 'ISO-2022-JP') }}* +*{{ null|convert_encoding('ISO-2022-JP', 'UTF-8')|convert_encoding('UTF-8', 'ISO-2022-JP') }}* --DATA-- return [] --EXPECT-- 愛していますか? +** +** diff --git a/tests/Fixtures/filters/escape.test b/tests/Fixtures/filters/escape.test index 131f5b4e7..b5b3f90f4 100644 --- a/tests/Fixtures/filters/escape.test +++ b/tests/Fixtures/filters/escape.test @@ -2,7 +2,11 @@ "escape" filter --TEMPLATE-- {{ "foo
"|e }} +*{{ ""|e }}* +*{{ null|e }}* --DATA-- return [] --EXPECT-- foo <br /> +** +** diff --git a/tests/Fixtures/filters/first.test b/tests/Fixtures/filters/first.test index b19f2eed7..6ffc18911 100644 --- a/tests/Fixtures/filters/first.test +++ b/tests/Fixtures/filters/first.test @@ -6,7 +6,8 @@ {{ '1234'|first }} {{ arr|first }} {{ 'Ä€é'|first }} -{{ ''|first }} +*{{ ''|first }}* +*{{ null|first }}* --DATA-- return ['arr' => new \ArrayObject([1, 2, 3, 4])] --EXPECT-- @@ -15,3 +16,5 @@ return ['arr' => new \ArrayObject([1, 2, 3, 4])] 1 1 Ä +** +** diff --git a/tests/Fixtures/filters/last.test b/tests/Fixtures/filters/last.test index f71896c77..2fdc63b33 100644 --- a/tests/Fixtures/filters/last.test +++ b/tests/Fixtures/filters/last.test @@ -6,7 +6,8 @@ {{ '1234'|last }} {{ arr|last }} {{ 'Ä€é'|last }} -{{ ''|last }} +*{{ ''|last }}* +*{{ null|last }}* --DATA-- return ['arr' => new \ArrayObject([1, 2, 3, 4])] --EXPECT-- @@ -15,3 +16,5 @@ return ['arr' => new \ArrayObject([1, 2, 3, 4])] 4 4 é +** +** diff --git a/tests/Fixtures/filters/length.test b/tests/Fixtures/filters/length.test index eb3f02334..6e0cd9927 100644 --- a/tests/Fixtures/filters/length.test +++ b/tests/Fixtures/filters/length.test @@ -7,6 +7,7 @@ {{ to_string_able|length }} {{ countable|length }} {{ iterator_aggregate|length }} +{{ ""|length }} {{ null|length }} {{ magic|length }} {{ non_countable|length }} @@ -34,6 +35,7 @@ return [ 42 3 0 +0 1 1 2 diff --git a/tests/Fixtures/filters/lower.test b/tests/Fixtures/filters/lower.test new file mode 100644 index 000000000..ec501e7ca --- /dev/null +++ b/tests/Fixtures/filters/lower.test @@ -0,0 +1,12 @@ +--TEST-- +"lower" filter +--TEMPLATE-- +{{ "I like Twig."|lower }} +*{{ ""|lower }}* +*{{ null|lower }}* +--DATA-- +return [] +--EXPECT-- +i like twig. +** +** diff --git a/tests/Fixtures/filters/number_format.test b/tests/Fixtures/filters/number_format.test index 7f1e2e16a..3d12a789c 100644 --- a/tests/Fixtures/filters/number_format.test +++ b/tests/Fixtures/filters/number_format.test @@ -7,6 +7,9 @@ {{ 20.25|number_format(2, ',') }} {{ 1020.25|number_format(2, ',') }} {{ 1020.25|number_format(2, ',', '.') }} +{{ '1020.25'|number_format(2, ',', '.') }} +{{ ''|number_format(2, ',', '.') }} +{{ null|number_format(2, ',', '.') }} --DATA-- return [] --EXPECT-- @@ -16,3 +19,6 @@ return [] 20,25 1,020,25 1.020,25 +1.020,25 +0,00 +0,00 diff --git a/tests/Fixtures/filters/replace.test b/tests/Fixtures/filters/replace.test index 1b9670a17..3b430cd5c 100644 --- a/tests/Fixtures/filters/replace.test +++ b/tests/Fixtures/filters/replace.test @@ -4,9 +4,13 @@ {{ "I liké %this% and %that%."|replace({'%this%': "foo", '%that%': "bar"}) }} {{ 'I like single replace operation only %that%'|replace({'%that%' : '%that%1'}) }} {{ 'I like %this% and %that%.'|replace(traversable) }} +*{{ ''|replace(traversable) }}* +*{{ null|replace(traversable) }}* --DATA-- return ['traversable' => new \ArrayObject(['%this%' => 'foo', '%that%' => 'bar'])] --EXPECT-- I liké foo and bar. I like single replace operation only %that%1 I like foo and bar. +** +** diff --git a/tests/Fixtures/filters/reverse.test b/tests/Fixtures/filters/reverse.test index 904e5839b..42b4dcc64 100644 --- a/tests/Fixtures/filters/reverse.test +++ b/tests/Fixtures/filters/reverse.test @@ -7,6 +7,8 @@ {{ {'a': 'c', 'b': 'a'}|reverse()|join(',') }} {{ {'a': 'c', 'b': 'a'}|reverse(preserveKeys=true)|join(glue=',') }} {{ {'a': 'c', 'b': 'a'}|reverse(preserve_keys=true)|join(glue=',') }} +*{{ ''|reverse }}* +*{{ null|reverse }}* --DATA-- return ['arr' => new \ArrayObject([1, 2, 3, 4])] --EXPECT-- @@ -16,3 +18,5 @@ tnemenèvé4321 a,c a,c a,c +** +** diff --git a/tests/Fixtures/filters/round.test b/tests/Fixtures/filters/round.test index 709237543..bb6afcf41 100644 --- a/tests/Fixtures/filters/round.test +++ b/tests/Fixtures/filters/round.test @@ -9,6 +9,10 @@ {{ 21.3|round(-1)}} {{ 21.3|round(-1, 'ceil')}} {{ 21.3|round(-1, 'floor')}} +{{ '21.3'|round(-1, 'floor')}} + +{{ ''|round(-1, 'floor')}} +{{ null|round(-1, 'floor')}} --DATA-- return [] --EXPECT-- @@ -20,3 +24,7 @@ return [] 20 30 20 +20 + +0 +0 diff --git a/tests/Fixtures/filters/spaceless.test b/tests/Fixtures/filters/spaceless.test index eadc1d496..166a7ea0b 100644 --- a/tests/Fixtures/filters/spaceless.test +++ b/tests/Fixtures/filters/spaceless.test @@ -2,7 +2,11 @@ "spaceless" filter --TEMPLATE-- {{ "
foo
"|spaceless }} +*{{ ""|spaceless }}* +*{{ null|spaceless }}* --DATA-- return [] --EXPECT--
foo
+** +** diff --git a/tests/Fixtures/filters/split.test b/tests/Fixtures/filters/split.test index 2bd46afa3..8b2a94d94 100644 --- a/tests/Fixtures/filters/split.test +++ b/tests/Fixtures/filters/split.test @@ -9,6 +9,8 @@ {{ baz|split('', 2)|join('-') }} {{ foo|split(',', -2)|join('-') }} {{ "hello0world"|split('0')|join('-') }} +*{{ ""|split(',')|join('-') }}* +*{{ null|split(',')|join('-') }}* --DATA-- return ['foo' => "one,two,three,four,five", 'baz' => '12345',] --EXPECT-- @@ -19,4 +21,6 @@ one-two-three,four,five 1-2-3-4-5 12-34-5 one-two-three -hello-world \ No newline at end of file +hello-world +** +** diff --git a/tests/Fixtures/filters/title.test b/tests/Fixtures/filters/title.test new file mode 100644 index 000000000..6f1c0c3e8 --- /dev/null +++ b/tests/Fixtures/filters/title.test @@ -0,0 +1,12 @@ +--TEST-- +"title" filter +--TEMPLATE-- +{{ "I like Twig."|title }} +*{{ ""|title }}* +*{{ null|title }}* +--DATA-- +return [] +--EXPECT-- +I Like Twig. +** +** diff --git a/tests/Fixtures/filters/trim.test b/tests/Fixtures/filters/trim.test index 432989ff1..141f86357 100644 --- a/tests/Fixtures/filters/trim.test +++ b/tests/Fixtures/filters/trim.test @@ -10,6 +10,12 @@ {{ "/ foo/"|trim("/", "left") }} {{ "/ foo/"|trim(character_mask="/", side="left") }} {{ " do nothing. "|trim("", "right") }} +*{{ ""|trim }}* +*{{ ""|trim("", "left") }}* +*{{ ""|trim("", "right") }}* +*{{ null|trim }}* +*{{ null|trim("", "left") }}* +*{{ null|trim("", "right") }}* --DATA-- return ['text' => " If you have some HTML it will be escaped. "] --EXPECT-- @@ -22,3 +28,9 @@ xxxI like Twig. foo/ foo/ do nothing. +** +** +** +** +** +** diff --git a/tests/Fixtures/filters/upper.test b/tests/Fixtures/filters/upper.test new file mode 100644 index 000000000..df415075e --- /dev/null +++ b/tests/Fixtures/filters/upper.test @@ -0,0 +1,12 @@ +--TEST-- +"upper" filter +--TEMPLATE-- +{{ "I like Twig."|upper }} +*{{ ""|upper }}* +*{{ null|upper }}* +--DATA-- +return [] +--EXPECT-- +I LIKE TWIG. +** +** diff --git a/tests/Fixtures/filters/urlencode.test b/tests/Fixtures/filters/urlencode.test index ac2369906..5db2e2ded 100644 --- a/tests/Fixtures/filters/urlencode.test +++ b/tests/Fixtures/filters/urlencode.test @@ -5,6 +5,8 @@ {{ {foo: "bar", number: 3, "spéßi%l": "e%c0d@d", "spa ce": ""}|url_encode|raw }} {{ {}|url_encode|default("default") }} {{ 'spéßi%le%c0d@dspa ce'|url_encode }} +*{{ ''|url_encode }}* +*{{ null|url_encode }}* --DATA-- return [] --EXPECT-- @@ -12,3 +14,5 @@ foo=bar&number=3&sp%C3%A9%C3%9Fi%25l=e%25c0d%40d&spa%20ce= foo=bar&number=3&sp%C3%A9%C3%9Fi%25l=e%25c0d%40d&spa%20ce= default sp%C3%A9%C3%9Fi%25le%25c0d%40dspa%20ce +** +**