symfony icon indicating copy to clipboard operation
symfony copied to clipboard

[HttpFoundation] Add `ParameterBag::getString` and add 3rd argument to make `getInt` more strict

Open GromNaN opened this issue 3 years ago • 1 comments

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) or getting an index (EasyAdmin)
  • I think there was an unidentified breaking change when we introduced return types #42507. 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

GromNaN avatar Dec 06 '22 23:12 GromNaN

I don't really like the extra argument to handle the BC layer because it means we're going to have to deprecate it in 7.1 + remove it in 8.0. Looks like a costly change. But I don't have a better idea other than just changing the behavior on 6.3 without this layer...

I'll take care of this process 😃.

But I am also tempted to change the behavior directly in 6.3. The modification to keep the current behavior is to replace $parameters->getInt('page') by (int) $parameters->get('page'). This change in user code is compatible with all previous version of Symfony and could be applied as a bugfix in all existing codebase that want to fallback to 0 in case of invalid value.

GromNaN avatar Dec 22 '22 16:12 GromNaN

But I am also tempted to change the behavior directly in 6.3.

I vote for this also. getInt() is for ints and the BC layer looks quite costly to me. I'd prefer getting rid of it.

nicolas-grekas avatar Mar 10 '23 18:03 nicolas-grekas

Dunno it it's a good idea but we could plan the BC break for 7.0 and announce the change with the following:

try {
    trigger_deprecation(...);
    trigger_error('Blahblah');
} catch (\ErrorException $e) {
    throw new BadRequestException(...);
}

By first a deprecation we announce this using the existing channel. The trigger_error would then turn into an exception in dev, and a regular non-blocking in prod. We would preserve the current behavior in 6.3+ and turn that into a real exception in 7.0.

WDYT?

nicolas-grekas avatar Mar 11 '23 09:03 nicolas-grekas

That's very clever. I'll try to implement this solution.

GromNaN avatar Mar 11 '23 16:03 GromNaN

After reflection by testing the implementation, as a user of this functions I prefer adding a 3rd argument to getInt instead of wrapping the method call with try-catch in order to convert the error (which we are not used to deal with) into the expected exception.

GromNaN avatar Mar 11 '23 17:03 GromNaN

Why would you wrap the call?

nicolas-grekas avatar Mar 11 '23 17:03 nicolas-grekas

I'm not sure to understand your proposition. Here is code samples from user/developer view.

Currently (<=6.2):

$inputs->getInt('invalid'); // returns 0 

With deprecation (6.3)

$inputs->getInt('invalid'); // returns 0 
// and trigger deprecation + error

Handling deprecation with try-catch (6.3) (my understanding of @nicolas-grekas proposition)

try {
    $inputs->getInt('invalid');
} catch (\ErrorException $e) {
    throw new BadRequestException($e->getMessage());
}
// throw an exception, the developer have to know which one.
// the deprecation notice is still triggered.

Handling deprecation with 3rd arg (6.3) (my first proposition)

$inputs->getInt('invalid', 0, true);
// throw an exception

Target in 7.0

$inputs->getInt('invalid', 0);
// throw an exception

GromNaN avatar Mar 11 '23 17:03 GromNaN

I meant to put the try/catch inside getInt, not outside!

nicolas-grekas avatar Mar 11 '23 17:03 nicolas-grekas

Hum, something like this?

    public function getInt(string $key, int $default = 0): int
    {
        try {
            return $this->filter(/* ... */);
        } catch (\UnexpectedValueException|BadRequestException  $e) {
            trigger_deprecation('symfony/http-foundation', '6.3', '...');
            trigger_error($e->getMessage());

            return 0;
        }
    }

GromNaN avatar Mar 15 '23 09:03 GromNaN

Updated. I'm not sure about the trigger_error: this introduces a very different behavior on dev vs prod. And I don't know how to test it since expectError is deprecated.

GromNaN avatar Mar 16 '23 10:03 GromNaN

or better: we (plan to) throw unless FILTER_NULL_ON_FAILURE is explicitly set, and we drop the error_on_failure option

This is a brilliant idea, let me explain it for the readers. Historically, filter_var returns false in case of invalid data. But this is a design problem because you can't tell a false from FILTER_VALIDATE_BOOLEAN from a false from an invalid value. So FILTER_NULL_ON_FAILURE was introduced in PHP 5.2 to allow differentiating errors. So we should always use this flag. We will use the absence of this flag to define our default behavior in Symfony 7.0: throw an exception in case of invalid data.

GromNaN avatar Mar 16 '23 17:03 GromNaN

Thank you @GromNaN.

fabpot avatar Mar 19 '23 20:03 fabpot