Skip to content

Commit 31e937a

Browse files
committed
throw an exception if required filter is not set
1 parent 23a8dff commit 31e937a

File tree

8 files changed

+256
-0
lines changed

8 files changed

+256
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Feature: Validate filters based upon filter description
2+
3+
@createSchema
4+
Scenario: Required filter should not throw an error if set
5+
When I am on "/filter_validators?foo=bar"
6+
Then the response status code should be 200
7+
8+
When I am on "/filter_validators?foo="
9+
Then the response status code should be 200
10+
11+
@dropSchema
12+
Scenario: Required filter should throw an error if not set
13+
When I am on "/filter_validators"
14+
Then the response status code should be 400
15+
And the JSON node "detail" should be equal to "query parameter `foo` is required"

src/Bridge/Symfony/Bundle/Resources/config/validator.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222

2323
<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="64" />
2424
</service>
25+
26+
<service id="ApiPlatform\Core\Filter\QueryParameterValidateListener">
27+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
28+
<argument type="service" id="validator" />
29+
<argument type="service" id="api_platform.filter_locator" />
30+
31+
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="16" />
32+
</service>
2533
</services>
2634

2735
</container>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Filter;
15+
16+
use ApiPlatform\Core\Api\FilterLocatorTrait;
17+
use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
18+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
19+
use ApiPlatform\Core\Util\RequestAttributesExtractor;
20+
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
21+
use Symfony\Component\Validator\Constraints as Assert;
22+
use Symfony\Component\Validator\Validator\ValidatorInterface as SymfonyValidatorInterface;
23+
24+
/**
25+
* Validate query parameters depending on filter description.
26+
*
27+
* @author Julien Deniau <[email protected]>
28+
*/
29+
class QueryParameterValidateListener
30+
{
31+
use FilterLocatorTrait;
32+
33+
private $resourceMetadataFactory;
34+
35+
private $validator;
36+
37+
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, SymfonyValidatorInterface $validator, $filterLocator)
38+
{
39+
$this->resourceMetadataFactory = $resourceMetadataFactory;
40+
$this->validator = $validator;
41+
$this->setFilterLocator($filterLocator);
42+
}
43+
44+
public function onKernelRequest(GetResponseEvent $event)
45+
{
46+
$request = $event->getRequest();
47+
if (
48+
!$request->isMethodSafe(false)
49+
|| !($attributes = RequestAttributesExtractor::extractAttributes($request))
50+
|| !isset($attributes['collection_operation_name'])
51+
|| 'get' !== ($operationName = $attributes['collection_operation_name'])
52+
) {
53+
return;
54+
}
55+
56+
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
57+
$resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);
58+
59+
foreach ($resourceFilters as $filterId) {
60+
if (!$filter = $this->getFilter($filterId)) {
61+
continue;
62+
}
63+
64+
foreach ($filter->getDescription($attributes['resource_class']) as $name => $data) {
65+
if ($data['required'] ?? false) {
66+
$requiredConstraint = new Assert\NotNull();
67+
$requiredConstraint->message = sprintf('query parameter `%s` is required', $name);
68+
$errorList = $this->validator->validate($request->query->get($name), $requiredConstraint);
69+
70+
if (count($errorList) > 0) {
71+
throw new ValidationException($errorList);
72+
}
73+
}
74+
}
75+
}
76+
}
77+
}

tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ private function getPartialContainerBuilderProphecy($test = false)
516516
'api_platform.listener.view.respond',
517517
'api_platform.listener.view.serialize',
518518
'api_platform.listener.view.validate',
519+
'ApiPlatform\Core\Filter\QueryParameterValidateListener',
519520
'api_platform.listener.view.write',
520521
'api_platform.metadata.extractor.xml',
521522
'api_platform.metadata.property.metadata_factory.cached',
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiProperty;
17+
use ApiPlatform\Core\Annotation\ApiResource;
18+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter\NotRequiredBarFilter;
19+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter\RequiredFooFilter;
20+
use Doctrine\ORM\Mapping as ORM;
21+
22+
/**
23+
* Filter Validator entity.
24+
*
25+
* @author Julien Deniau <[email protected]>
26+
*
27+
* @ApiResource(attributes={
28+
* "filters"={
29+
* NotRequiredBarFilter::class,
30+
* RequiredFooFilter::class
31+
* }
32+
* })
33+
* @ORM\Entity
34+
*/
35+
class FilterValidator
36+
{
37+
/**
38+
* @var int The id
39+
*
40+
* @ORM\Column(type="integer")
41+
* @ORM\Id
42+
* @ORM\GeneratedValue(strategy="AUTO")
43+
*/
44+
private $id;
45+
46+
/**
47+
* @var string A name
48+
*
49+
* @ORM\Column
50+
* @ApiProperty(iri="http://schema.org/name")
51+
*/
52+
private $name;
53+
54+
public function getId()
55+
{
56+
return $this->id;
57+
}
58+
59+
public function setId($id)
60+
{
61+
$this->id = $id;
62+
}
63+
64+
public function setName($name)
65+
{
66+
$this->name = $name;
67+
}
68+
69+
public function getName()
70+
{
71+
return $this->name;
72+
}
73+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter;
15+
16+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter;
17+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18+
use Doctrine\ORM\QueryBuilder;
19+
20+
class NotRequiredBarFilter extends AbstractFilter
21+
{
22+
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
23+
{
24+
}
25+
26+
// This function is only used to hook in documentation generators (supported by Swagger and Hydra)
27+
public function getDescription(string $resourceClass): array
28+
{
29+
return [
30+
'bar' => [
31+
'property' => 'bar',
32+
'type' => 'string',
33+
'required' => false,
34+
],
35+
];
36+
}
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter;
15+
16+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter;
17+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18+
use Doctrine\ORM\QueryBuilder;
19+
20+
class RequiredFooFilter extends AbstractFilter
21+
{
22+
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
23+
{
24+
}
25+
26+
// This function is only used to hook in documentation generators (supported by Swagger and Hydra)
27+
public function getDescription(string $resourceClass): array
28+
{
29+
return [
30+
'foo' => [
31+
'property' => 'foo',
32+
'type' => 'string',
33+
'required' => true,
34+
],
35+
];
36+
}
37+
}

tests/Fixtures/app/config/config_test.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ services:
164164
parent: 'api_platform.serializer.property_filter'
165165
tags: [ { name: 'api_platform.filter', id: 'my_dummy.property' } ]
166166

167+
ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter\RequiredFooFilter:
168+
arguments: [ '@doctrine' ]
169+
tags: [ 'api_platform.filter' ]
170+
171+
ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter\NotRequiredBarFilter:
172+
arguments: [ '@doctrine' ]
173+
tags: [ 'api_platform.filter' ]
174+
167175
app.config_dummy_resource.action:
168176
class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\Action\ConfigCustom'
169177
arguments: ['@api_platform.item_data_provider']

0 commit comments

Comments
 (0)