Skip to content

throw an exception if required filter is not set#1692

Merged
soyuka merged 8 commits into
api-platform:masterfrom
jdeniau:jd-feat-queryParameterValidation
Jun 4, 2018
Merged

throw an exception if required filter is not set#1692
soyuka merged 8 commits into
api-platform:masterfrom
jdeniau:jd-feat-queryParameterValidation

Conversation

@jdeniau

@jdeniau jdeniau commented Feb 8, 2018

Copy link
Copy Markdown
Contributor
Q A
Bug fix? kind of
New feature? yes
BC breaks? if filter config was wrongly set to required=true
Deprecations? no
Tests pass? yes
Fixed tickets #1627
License MIT
Doc PR Doc PR will follow if PR is approved

This PR is a start of query parameter validation.

It does only check required=true filter for now but I will open other PR to include the rest of OpenAPI specification on field validation.

There are some points that I am not really sure of:

  • namespace of the new QueryParameterValidateLister: I put it in Core\Validator\EventListener but maybe in Symfony\Bridge ?
  • I set the priority of the event listener to 16, this way, it is the first event in the queue (according to the documentation)

Otherwise, the code is quite simple and leverage Symfony validation system.

Thanks !

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch from 653213d to e982f80 Compare February 8, 2018 15:27
@dunglas

dunglas commented Feb 8, 2018

Copy link
Copy Markdown
Member

I'm glad that you work on this! Thanks.

Do we really need a dependency to the Symfony validator for that? Can't we just use the filter extension? It will reusing API Platform in a non-Symfony context (PSR-7 and Laravel support is on our roadmap).

About the namespace, I would suggest to put it in the Filter namespace.

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch from e982f80 to af2baa6 Compare February 8, 2018 15:37
@jdeniau

jdeniau commented Feb 8, 2018

Copy link
Copy Markdown
Contributor Author

@dunglas I used Symfony validation for several reason:

  • api-platform automatically throw a 400 exception when a ConstraintViolationList is throwned
  • api-platform do already depends on a validator service (is it deprecated ?)
  • OpenApi declare several possible requirements that seems to work really great with Symfony constraints system

Or maybe I did not really understand your comment ?

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch from af2baa6 to 1930621 Compare February 8, 2018 15:51
@dunglas

dunglas commented Feb 8, 2018

Copy link
Copy Markdown
Member

The integration with the validator is 100% optional (as with Doctrine etc), in fact it's only used in the bridge for convenience for the end user, but IIRC, not for any "core" feature like this one.

Regarding the 400, it's just one line to add to support a new exception: https://github.com/api-platform/core/blob/master/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php#L251

@jdeniau

jdeniau commented Feb 8, 2018

Copy link
Copy Markdown
Contributor Author

@dunglas OK for the ContraintViolationException, I can throw an api-platform specific exception, but don't you think it will be reinventing the wheel to re-implement every OpenApi validation ? (enum, pattern, min / max, empty value, type, etc.) ?

I think it will be a big copy/paste from symfony's constraints, don't you ?

About the FilterExtension, are you saying that I should drop the event listener and put the code in the FilterExtension ?

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch from 1930621 to d43e385 Compare February 14, 2018 15:50
@jdeniau

jdeniau commented Feb 15, 2018

Copy link
Copy Markdown
Contributor Author

@dunglas I remove the dependency on Symfony validator service.

About using the FilterExtension, I did not, because FilterExtension is Doctrine-specific and will not work when using resources that are not Doctrine's.

I think it's good for merge. I will make other PR's implementing the rest of OpenApi parameters specification after that.

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch from 72845d9 to e2212dc Compare February 15, 2018 08:21

@dunglas dunglas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one :)

namespace ApiPlatform\Core\Exception;

/**
* Deserialization exception.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be updated

/**
* Gets constraint violations related to this exception.
*
* @return ConstraintViolationListInterface

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use a return type instead.

use Symfony\Component\HttpKernel\Event\GetResponseEvent;

/**
* Validate query parameters depending on filter description.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validates

}

foreach ($filter->getDescription($attributes['resource_class']) as $name => $data) {
if ($data['required'] ?? false) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you invert this condition to reduce the complexity?

if (!($data['required'] ?? false)) {
    continue;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed a litte this part to prepare the next step that will validate several parameter rules, but this should be more understandable though

$errorList = [];

if (null === $request->query->get($name)) {
$errorList[] = sprintf('Query parameter `%s` is required', $name);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change the backticks with double quotes for consistency with other exceptions?

$errorList[] = sprintf('Query parameter `%s` is required', $name);
}

if (count($errorList) > 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be simplified in if ($errorList) {

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch from e2212dc to 47877d6 Compare February 15, 2018 09:39
@jdeniau

jdeniau commented Feb 15, 2018

Copy link
Copy Markdown
Contributor Author

@dunglas done

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch 2 times, most recently from 3d05d37 to c76a40f Compare February 21, 2018 15:29
@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch from 7add831 to 40b6fe2 Compare February 22, 2018 07:29
foreach ($filter->getDescription($attributes['resource_class']) as $name => $data) {
$errorList = [];

if (($data['required'] ?? false) && null === $request->query->get($name)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition will not work with serializer's filters (assuming they are required) for example. Indeed, property[] is not equal to property.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry but I did not really understand what you mean here ?

Can you provide an example when this does not work ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's take the description of the property filter:

public function getDescription(string $resourceClass): array
{
return [
"$this->parameterName[]" => [
'property' => null,
'type' => 'string',
'required' => false,
],
];
}

And let's say required is equal to true for our example.

So, the $name var will be equal to property[] here. But with ?property['relatedDummy'][]=name the name will be property in the parameter bag and $request->query->get('property[]') will return null...

*
* @author Julien Deniau <julien.deniau@gmail.com>
*/
class QueryParameterValidateListener

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a unit test please?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it usefull ? It's fully tested with behat tests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, Behat is only for integration and acceptance tests. You also have to unit test your class.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch from 40b6fe2 to 8a39942 Compare March 8, 2018 23:02
@jdeniau

jdeniau commented Mar 9, 2018

Copy link
Copy Markdown
Contributor Author

@meyerbaptiste Done for the unit tests and the "array notation" (but I'm not really happy with the implementation though :| )

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch 2 times, most recently from bb98949 to 0ddfa2b Compare March 12, 2018 13:57
continue;
}

if (false !== strpos($name, '[')) { // array notation of filter

@meyerbaptiste meyerbaptiste Mar 13, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have another dirty solution:

parse_str($name, $query);
$name = array_keys($query)[0] ?? '';

It works with:

  • ''
  • 'foo'
  • 'foo[]'
  • 'foo[bar]'
  • 'foo[bar][]'
  • 'foo[bar][baz]'
  • etc.

😄

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch 2 times, most recently from 6c5432c to e8375de Compare March 23, 2018 13:21
private function isRequiredFilterValid($name, $request): bool
{
$matches = [];
if (false === mb_parse_str($name, $matches)) {

@meyerbaptiste meyerbaptiste Mar 23, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huum, this requires the mbstring extension... And the mb_parse_str() function is not implemented by symfony/polyfill-mbstring!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, I though it was included since PHP7, my bad, I remove this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch 2 times, most recently from 1388e44 to a04ee65 Compare March 23, 2018 15:37
@jdeniau

jdeniau commented Mar 27, 2018

Copy link
Copy Markdown
Contributor Author

@meyerbaptiste changed regex → parse_str

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch from a04ee65 to ffb46ea Compare March 28, 2018 14:55
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;

class ArrayRequiredFilter extends AbstractFilter

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be final

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;

class RequiredFilter extends AbstractFilter

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be final


ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter\ArrayRequiredFilter:
arguments: [ '@doctrine' ]
tags: [ 'api_platform.filter' ]

@soyuka soyuka May 24, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're not using spaces after [ IIRC in this case?

private function isRequiredFilterValid($name, $request): bool
{
$matches = [];
parse_str($name, $matches);

@teohhanhui teohhanhui May 24, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've mentioned this a long time ago, but we really should stop using bad built-in PHP functions like parse_str. We should seriously think about using a proper URI parsing library instead: https://uri.thephpleague.com/5.0/

https://uri.thephpleague.com/5.0/components/parsers/#queryparserextract

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really necessary for this use case? Maybe can we do that at some point in another PR and replace all calls to parse_str in 1 time.

@teohhanhui

teohhanhui commented May 24, 2018

Copy link
Copy Markdown
Contributor

@dunglas I think it's better if we avoid giving the filter implementations the responsibility of checking required filters. It's not their job and it doesn't need to be. But I agree with extracting all logic into a separate class, so as to keep the event listeners really thin / "dumb".

@dunglas

dunglas commented May 24, 2018

Copy link
Copy Markdown
Member

Introducing a new service and using it in the listener looks good to me.

@jdeniau

jdeniau commented May 24, 2018

Copy link
Copy Markdown
Contributor Author

@soyuka about the "light" listener, the separation between the listener and the validation has been made in #1723 ( https://github.com/api-platform/core/pull/1723/files#diff-91ffc72a7db30ec816871e7141558def exactly ) : I prefer keeping code simple first, and re-factor it when needed.
With this modification, I can extract a FilterListValidator, but the only code it will contain is a foreach($validatorList as $validator) { doValidate(); }

@dunglas About the namespace, we talked about it previously, but I can move the listener in EventListener and move back the Filter validator in Filter namespace.

Can we keep the listener "as-is" for this PR, and make the refactoring in #1723, as the purpose of this PR is like a POC before #1723 ? (it's quite painful to rebase each time, and I think it will be more painful this time as the code is several months old).

@dunglas

dunglas commented May 24, 2018

Copy link
Copy Markdown
Member

LGTM.

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch 3 times, most recently from c5cc04b to 462751b Compare May 26, 2018 09:35
@jdeniau

jdeniau commented May 26, 2018

Copy link
Copy Markdown
Contributor Author

@soyuka done

@jdeniau
jdeniau force-pushed the jd-feat-queryParameterValidation branch from 462751b to 1e353af Compare June 4, 2018 07:45
@jdeniau

jdeniau commented Jun 4, 2018

Copy link
Copy Markdown
Contributor Author

@dunglas @meyerbaptiste @Simperfit @soyuka everything is OK on my part I think.

Waiting for a merge on master to work on the second PR that will check all documented fields

@soyuka
soyuka merged commit d2250c1 into api-platform:master Jun 4, 2018
@soyuka

soyuka commented Jun 4, 2018

Copy link
Copy Markdown
Member

Thanks @jdeniau !

@jdeniau
jdeniau deleted the jd-feat-queryParameterValidation branch June 4, 2018 09:24
soyuka added a commit that referenced this pull request Dec 17, 2019
…ation

More query parameter validation (follow #1692)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants