Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6adba59

Browse files
committed
feature #48525 [HttpFoundation] Add ParameterBag::getString() and deprecate accepting invalid values (GromNaN)
This PR was merged into the 6.3 branch. Discussion ---------- [HttpFoundation] Add `ParameterBag::getString()` and deprecate accepting invalid values | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | yes | Tickets | Fix #44787 #48219 | License | MIT | Doc PR | symfony/symfony-docs#... There were a lot of discussions on #44787 regarding the implementation. The main debate is to determine the right behavior when the value is invalid: try to convert the value, throw an exception, return the default value or the falsy? I added plenty of test cases for the methods `getAlpha`, `getAlnum`, `getDigits`, `getInt`, ~`getInteger`~, `getString` so that we can discuss the expected result for each input value. My goals: - Provide safe methods to get values in the expected type. Example: The parameters being generally of type `string`, the `getString` method is useful to be sure we don't get an array. - Reduce deprecations on methods that exist since Symfony 2.0, in one of the most popular Symfony component. How are these getter methods used? - Most of the time, parameter values are `string` (from routing, query string, request payload). `get` is used but does not validate the return type. - `getInt` is used for [pagination (UX Autocomplete)](https://github.com/symfony/ux-autocomplete/blob/697f1cb4914480b811d978efe031a6f4a0dc3814/src/Controller/EntityAutocompleteController.php#L41) or [getting an index (EasyAdmin)](https://github.com/EasyCorp/EasyAdminBundle/blob/f210bd1e494b699dec54d2ef29302db1211267b5/src/Context/AdminContext.php#L157-L158) - I think there was an unidentified breaking change when we introduced return types [#42507](https://github.com/symfony/symfony/pull/42507/files#diff-04f3b8ea71029f48853b804129631aeb9bf3dad7a7a398feb9568bf5397d0e69L117). Methods `getAlpha`, `getAlnum` and `getDigits` could return an array, but their return type have been modified to `string`, resulting in a `TypeError`. [example of use](https://github.com/asaaa1997/Licencjat/blob/8c10abaf87120c904557977ae14284c7d443c9a7/src/Controller/QuizController.php#L164) Commits ------- 172f0a7 [HttpFoundation] Add `ParameterBag::getString()` and deprecate accepting invalid values
2 parents 7fc526e + 172f0a7 commit 6adba59

File tree

6 files changed

+311
-26
lines changed

6 files changed

+311
-26
lines changed

UPGRADE-6.3.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ HttpFoundation
6363

6464
* `Response::sendHeaders()` now takes an optional `$statusCode` parameter
6565

66+
HttpFoundation
67+
--------------
68+
69+
* Deprecate conversion of invalid values in `ParameterBag::getInt()` and `ParameterBag::getBoolean()`
70+
* Deprecate ignoring invalid values when using `ParameterBag::filter()`, unless flag `FILTER_NULL_ON_FAILURE` is set
71+
6672
HttpKernel
6773
----------
6874

src/Symfony/Component/HttpFoundation/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ CHANGELOG
44
6.3
55
---
66

7+
* Calling `ParameterBag::getDigit()`, `getAlnum()`, `getAlpha()` on an `array` throws a `UnexpectedValueException` instead of a `TypeError`
8+
* Add `ParameterBag::getString()` to convert a parameter into string and throw an exception if the value is invalid
79
* Add `ParameterBag::getEnum()`
810
* Create migration for session table when pdo handler is used
911
* Add support for Relay PHP extension for Redis
1012
* The `Response::sendHeaders()` method now takes an optional HTTP status code as parameter, allowing to send informational responses such as Early Hints responses (103 status code)
13+
* Deprecate conversion of invalid values in `ParameterBag::getInt()` and `ParameterBag::getBoolean()`,
14+
* Deprecate ignoring invalid values when using `ParameterBag::filter()`, unless flag `FILTER_NULL_ON_FAILURE` is set
1115

1216
6.2
1317
---

src/Symfony/Component/HttpFoundation/InputBag.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ public function getEnum(string $key, string $class, \BackedEnum $default = null)
9292
}
9393
}
9494

95+
/**
96+
* Returns the parameter value converted to string.
97+
*/
98+
public function getString(string $key, string $default = ''): string
99+
{
100+
// Shortcuts the parent method because the validation on scalar is already done in get().
101+
return (string) $this->get($key, $default);
102+
}
103+
95104
public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed
96105
{
97106
$value = $this->has($key) ? $this->all()[$key] : $default;
@@ -109,6 +118,22 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER
109118
throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null)));
110119
}
111120

112-
return filter_var($value, $filter, $options);
121+
$options['flags'] ??= 0;
122+
$nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE;
123+
$options['flags'] |= \FILTER_NULL_ON_FAILURE;
124+
125+
$value = filter_var($value, $filter, $options);
126+
127+
if (null !== $value || $nullOnFailure) {
128+
return $value;
129+
}
130+
131+
$method = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1];
132+
$method = ($method['object'] ?? null) === $this ? $method['function'] : 'filter';
133+
$hint = 'filter' === $method ? 'pass' : 'use method "filter()" with';
134+
135+
trigger_deprecation('symfony/http-foundation', '6.3', 'Ignoring invalid values when using "%s::%s(\'%s\')" is deprecated and will throw a "%s" in 7.0; '.$hint.' flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', $this::class, $method, $key, BadRequestException::class);
136+
137+
return false;
113138
}
114139
}

src/Symfony/Component/HttpFoundation/ParameterBag.php

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,40 +114,53 @@ public function remove(string $key)
114114
*/
115115
public function getAlpha(string $key, string $default = ''): string
116116
{
117-
return preg_replace('/[^[:alpha:]]/', '', $this->get($key, $default));
117+
return preg_replace('/[^[:alpha:]]/', '', $this->getString($key, $default));
118118
}
119119

120120
/**
121121
* Returns the alphabetic characters and digits of the parameter value.
122122
*/
123123
public function getAlnum(string $key, string $default = ''): string
124124
{
125-
return preg_replace('/[^[:alnum:]]/', '', $this->get($key, $default));
125+
return preg_replace('/[^[:alnum:]]/', '', $this->getString($key, $default));
126126
}
127127

128128
/**
129129
* Returns the digits of the parameter value.
130130
*/
131131
public function getDigits(string $key, string $default = ''): string
132132
{
133-
// we need to remove - and + because they're allowed in the filter
134-
return str_replace(['-', '+'], '', $this->filter($key, $default, \FILTER_SANITIZE_NUMBER_INT));
133+
return preg_replace('/[^[:digit:]]/', '', $this->getString($key, $default));
134+
}
135+
136+
/**
137+
* Returns the parameter as string.
138+
*/
139+
public function getString(string $key, string $default = ''): string
140+
{
141+
$value = $this->get($key, $default);
142+
if (!\is_scalar($value) && !$value instanceof \Stringable) {
143+
throw new \UnexpectedValueException(sprintf('Parameter value "%s" cannot be converted to "string".', $key));
144+
}
145+
146+
return (string) $value;
135147
}
136148

137149
/**
138150
* Returns the parameter value converted to integer.
139151
*/
140152
public function getInt(string $key, int $default = 0): int
141153
{
142-
return (int) $this->get($key, $default);
154+
// In 7.0 remove the fallback to 0, in case of failure an exception will be thrown
155+
return $this->filter($key, $default, \FILTER_VALIDATE_INT, ['flags' => \FILTER_REQUIRE_SCALAR]) ?: 0;
143156
}
144157

145158
/**
146159
* Returns the parameter value converted to boolean.
147160
*/
148161
public function getBoolean(string $key, bool $default = false): bool
149162
{
150-
return $this->filter($key, $default, \FILTER_VALIDATE_BOOL);
163+
return $this->filter($key, $default, \FILTER_VALIDATE_BOOL, ['flags' => \FILTER_REQUIRE_SCALAR]);
151164
}
152165

153166
/**
@@ -178,7 +191,8 @@ public function getEnum(string $key, string $class, \BackedEnum $default = null)
178191
/**
179192
* Filter key.
180193
*
181-
* @param int $filter FILTER_* constant
194+
* @param int $filter FILTER_* constant
195+
* @param int|array{flags?: int, options?: array} $options Flags from FILTER_* constants
182196
*
183197
* @see https://php.net/filter-var
184198
*/
@@ -196,11 +210,31 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER
196210
$options['flags'] = \FILTER_REQUIRE_ARRAY;
197211
}
198212

213+
if (\is_object($value) && !$value instanceof \Stringable) {
214+
throw new \UnexpectedValueException(sprintf('Parameter value "%s" cannot be filtered.', $key));
215+
}
216+
199217
if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) {
200218
throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null)));
201219
}
202220

203-
return filter_var($value, $filter, $options);
221+
$options['flags'] ??= 0;
222+
$nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE;
223+
$options['flags'] |= \FILTER_NULL_ON_FAILURE;
224+
225+
$value = filter_var($value, $filter, $options);
226+
227+
if (null !== $value || $nullOnFailure) {
228+
return $value;
229+
}
230+
231+
$method = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1];
232+
$method = ($method['object'] ?? null) === $this ? $method['function'] : 'filter';
233+
$hint = 'filter' === $method ? 'pass' : 'use method "filter()" with';
234+
235+
trigger_deprecation('symfony/http-foundation', '6.3', 'Ignoring invalid values when using "%s::%s(\'%s\')" is deprecated and will throw an "%s" in 7.0; '.$hint.' flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', $this::class, $method, $key, \UnexpectedValueException::class);
236+
237+
return false;
204238
}
205239

206240
/**

src/Symfony/Component/HttpFoundation/Tests/InputBagTest.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@
1212
namespace Symfony\Component\HttpFoundation\Tests;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
1516
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
1617
use Symfony\Component\HttpFoundation\InputBag;
1718
use Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum;
1819

1920
class InputBagTest extends TestCase
2021
{
22+
use ExpectDeprecationTrait;
23+
2124
public function testGet()
2225
{
2326
$bag = new InputBag(['foo' => 'bar', 'null' => null, 'int' => 1, 'float' => 1.0, 'bool' => false, 'stringable' => new class() implements \Stringable {
@@ -36,6 +39,58 @@ public function __toString(): string
3639
$this->assertFalse($bag->get('bool'), '->get() gets the value of a bool parameter');
3740
}
3841

42+
/**
43+
* @group legacy
44+
*/
45+
public function testGetIntError()
46+
{
47+
$this->expectDeprecation('Since symfony/http-foundation 6.3: Ignoring invalid values when using "Symfony\Component\HttpFoundation\InputBag::getInt(\'foo\')" is deprecated and will throw a "Symfony\Component\HttpFoundation\Exception\BadRequestException" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.');
48+
49+
$bag = new InputBag(['foo' => 'bar']);
50+
$result = $bag->getInt('foo');
51+
$this->assertSame(0, $result);
52+
}
53+
54+
/**
55+
* @group legacy
56+
*/
57+
public function testGetBooleanError()
58+
{
59+
$this->expectDeprecation('Since symfony/http-foundation 6.3: Ignoring invalid values when using "Symfony\Component\HttpFoundation\InputBag::getBoolean(\'foo\')" is deprecated and will throw a "Symfony\Component\HttpFoundation\Exception\BadRequestException" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.');
60+
61+
$bag = new InputBag(['foo' => 'bar']);
62+
$result = $bag->getBoolean('foo');
63+
$this->assertFalse($result);
64+
}
65+
66+
public function testGetString()
67+
{
68+
$bag = new InputBag(['integer' => 123, 'bool_true' => true, 'bool_false' => false, 'string' => 'abc', 'stringable' => new class() implements \Stringable {
69+
public function __toString(): string
70+
{
71+
return 'strval';
72+
}
73+
}]);
74+
75+
$this->assertSame('123', $bag->getString('integer'), '->getString() gets a value of parameter as string');
76+
$this->assertSame('abc', $bag->getString('string'), '->getString() gets a value of parameter as string');
77+
$this->assertSame('', $bag->getString('unknown'), '->getString() returns zero if a parameter is not defined');
78+
$this->assertSame('foo', $bag->getString('unknown', 'foo'), '->getString() returns the default if a parameter is not defined');
79+
$this->assertSame('1', $bag->getString('bool_true'), '->getString() returns "1" if a parameter is true');
80+
$this->assertSame('', $bag->getString('bool_false', 'foo'), '->getString() returns an empty empty string if a parameter is false');
81+
$this->assertSame('strval', $bag->getString('stringable'), '->getString() gets a value of a stringable paramater as string');
82+
}
83+
84+
public function testGetStringExceptionWithArray()
85+
{
86+
$bag = new InputBag(['key' => ['abc']]);
87+
88+
$this->expectException(BadRequestException::class);
89+
$this->expectExceptionMessage('Input value "key" contains a non-scalar value.');
90+
91+
$bag->getString('key');
92+
}
93+
3994
public function testGetDoesNotUseDeepByDefault()
4095
{
4196
$bag = new InputBag(['foo' => ['bar' => 'moo']]);
@@ -126,4 +181,34 @@ public function testGetEnumThrowsExceptionWithInvalidValue()
126181

127182
$this->assertNull($bag->getEnum('invalid-value', FooEnum::class));
128183
}
184+
185+
public function testGetAlnumExceptionWithArray()
186+
{
187+
$bag = new InputBag(['word' => ['foo_BAR_012']]);
188+
189+
$this->expectException(BadRequestException::class);
190+
$this->expectExceptionMessage('Input value "word" contains a non-scalar value.');
191+
192+
$bag->getAlnum('word');
193+
}
194+
195+
public function testGetAlphaExceptionWithArray()
196+
{
197+
$bag = new InputBag(['word' => ['foo_BAR_012']]);
198+
199+
$this->expectException(BadRequestException::class);
200+
$this->expectExceptionMessage('Input value "word" contains a non-scalar value.');
201+
202+
$bag->getAlpha('word');
203+
}
204+
205+
public function testGetDigitsExceptionWithArray()
206+
{
207+
$bag = new InputBag(['word' => ['foo_BAR_012']]);
208+
209+
$this->expectException(BadRequestException::class);
210+
$this->expectExceptionMessage('Input value "word" contains a non-scalar value.');
211+
212+
$bag->getDigits('word');
213+
}
129214
}

0 commit comments

Comments
 (0)