Skip to content

Commit d65c84b

Browse files
authored
Add a flag to disable all request listeners (#909)
1 parent f1d44be commit d65c84b

File tree

14 files changed

+282
-7
lines changed

14 files changed

+282
-7
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"phpdocumentor/type-resolver": "^0.2",
4141
"phpunit/phpunit": "^5.6.8",
4242
"psr/log": "^1.0",
43+
"sensio/framework-extra-bundle": "^3.0",
4344
"symfony/asset": "^2.7 || ^3.0",
4445
"symfony/cache": "^3.1",
4546
"symfony/config": "^3.2",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
Feature: Custom operation
2+
As a client software developer
3+
I need to be able to create custom operations
4+
5+
@createSchema
6+
Scenario: Custom normalization operation
7+
When I send a "POST" request to "/custom/denormalization"
8+
And I add "Content-Type" header equal to "application/ld+json"
9+
Then the JSON should be equal to:
10+
"""
11+
{
12+
"@context": "/contexts/CustomActionDummy",
13+
"@id": "/custom_action_dummies/1",
14+
"@type": "CustomActionDummy",
15+
"id": 1,
16+
"foo": "custom!"
17+
}
18+
"""
19+
20+
@dropSchema
21+
Scenario: Custom normalization operation
22+
When I send a "GET" request to "/custom/1/normalization"
23+
Then the JSON should be equal to:
24+
"""
25+
{
26+
"id": 1,
27+
"foo": "foo"
28+
}
29+
"""

src/Bridge/Symfony/Validator/EventListener/ValidateListener.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,17 @@ public function __construct(ValidatorInterface $validator, ResourceMetadataFacto
4545
public function onKernelView(GetResponseForControllerResultEvent $event)
4646
{
4747
$request = $event->getRequest();
48+
if ($request->isMethodSafe(false) || $request->isMethod(Request::METHOD_DELETE)) {
49+
return;
50+
}
51+
4852
try {
4953
$attributes = RequestAttributesExtractor::extractAttributes($request);
5054
} catch (RuntimeException $e) {
5155
return;
5256
}
5357

54-
if ($request->isMethodSafe(false) || $request->isMethod(Request::METHOD_DELETE)) {
58+
if (!$attributes['receive']) {
5559
return;
5660
}
5761

src/EventListener/DeserializeListener.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ public function onKernelRequest(GetResponseEvent $event)
5555
return;
5656
}
5757

58+
if (!$attributes['receive']) {
59+
return;
60+
}
61+
5862
$format = $this->getFormat($request);
5963
$context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
6064

src/EventListener/ReadListener.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ public function onKernelRequest(GetResponseEvent $event)
5252
return;
5353
}
5454

55+
if (!$attributes['receive']) {
56+
return;
57+
}
58+
5559
if (isset($attributes['collection_operation_name'])) {
5660
$data = $this->getCollectionData($request, $attributes);
5761
} else {

src/Util/RequestAttributesExtractor.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ public static function extractAttributes(Request $request)
5656
throw new RuntimeException('One of the request attribute "_api_collection_operation_name" or "_api_item_operation_name" must be defined.');
5757
}
5858

59+
if (null === $apiRequest = $request->attributes->get('_api_receive')) {
60+
$result['receive'] = true;
61+
} else {
62+
$result['receive'] = (bool) $apiRequest;
63+
}
64+
5965
return $result;
6066
}
6167
}

tests/Bridge/Symfony/Validator/EventListener/ValidateListenerTest.php

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@
2626
*/
2727
class ValidateListenerTest extends \PHPUnit_Framework_TestCase
2828
{
29+
public function testNotAnApiPlatformRequest()
30+
{
31+
$validatorProphecy = $this->prophesize(ValidatorInterface::class);
32+
$validatorProphecy->validate()->shouldNotBeCalled();
33+
$validator = $validatorProphecy->reveal();
34+
35+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
36+
$resourceMetadataFactoryProphecy->create()->shouldNotBeCalled();
37+
$resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();
38+
39+
$request = new Request();
40+
$request->setMethod('POST');
41+
42+
$event = $this->prophesize(GetResponseForControllerResultEvent::class);
43+
$event->getRequest()->willReturn($request)->shouldBeCalled();
44+
45+
$listener = new ValidateListener($validator, $resourceMetadataFactory);
46+
$listener->onKernelView($event->reveal());
47+
}
48+
2949
public function testValidatorIsCalled()
3050
{
3151
$data = new DummyEntity();
@@ -41,6 +61,21 @@ public function testValidatorIsCalled()
4161
$validationViewListener->onKernelView($event);
4262
}
4363

64+
public function testDoNotCallWhenReceiveFlagIsFalse()
65+
{
66+
$data = new DummyEntity();
67+
$expectedValidationGroups = ['a', 'b', 'c'];
68+
69+
$validatorProphecy = $this->prophesize(ValidatorInterface::class);
70+
$validatorProphecy->validate($data, null, $expectedValidationGroups)->shouldNotBeCalled();
71+
$validator = $validatorProphecy->reveal();
72+
73+
list($resourceMetadataFactory, $event) = $this->createEventObject($expectedValidationGroups, $data, false);
74+
75+
$validationViewListener = new ValidateListener($validator, $resourceMetadataFactory);
76+
$validationViewListener->onKernelView($event);
77+
}
78+
4479
/**
4580
* @expectedException \ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException
4681
*/
@@ -54,7 +89,7 @@ public function testThrowsValidationExceptionWithViolationsFound()
5489
$violations = $violationsProphecy->reveal();
5590

5691
$validatorProphecy = $this->prophesize(ValidatorInterface::class);
57-
$validatorProphecy->validate($data, null, $expectedValidationGroups)->shouldBeCalled()->willReturn($violations)->shouldBeCalled();
92+
$validatorProphecy->validate($data, null, $expectedValidationGroups)->willReturn($violations)->shouldBeCalled();
5893
$validator = $validatorProphecy->reveal();
5994

6095
list($resourceMetadataFactory, $event) = $this->createEventObject($expectedValidationGroups, $data);
@@ -66,10 +101,11 @@ public function testThrowsValidationExceptionWithViolationsFound()
66101
/**
67102
* @param array $expectedValidationGroups
68103
* @param mixed $data
104+
* @param bool $receive
69105
*
70106
* @return array
71107
*/
72-
private function createEventObject($expectedValidationGroups, $data)
108+
private function createEventObject($expectedValidationGroups, $data, bool $receive = true)
73109
{
74110
$resourceMetadata = new ResourceMetadata(null, null, null, [
75111
'create' => [
@@ -78,7 +114,9 @@ private function createEventObject($expectedValidationGroups, $data)
78114
]);
79115

80116
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
81-
$resourceMetadataFactoryProphecy->create(DummyEntity::class)->willReturn($resourceMetadata)->shouldBeCalled();
117+
if ($receive) {
118+
$resourceMetadataFactoryProphecy->create(DummyEntity::class)->willReturn($resourceMetadata)->shouldBeCalled();
119+
}
82120
$resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();
83121

84122
$kernel = $this->prophesize(HttpKernelInterface::class)->reveal();
@@ -87,6 +125,7 @@ private function createEventObject($expectedValidationGroups, $data)
87125
'_api_item_operation_name' => 'create',
88126
'_api_format' => 'json',
89127
'_api_mime_type' => 'application/json',
128+
'_api_receive' => $receive,
90129
]);
91130

92131
$request->setMethod(Request::METHOD_POST);

tests/EventListener/DeserializeListenerTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,24 @@ public function testDoNotCallWhenRequestNotManaged()
6161
$listener->onKernelRequest($eventProphecy->reveal());
6262
}
6363

64+
public function testDoNotCallWhenReceiveFlagIsFalse()
65+
{
66+
$eventProphecy = $this->prophesize(GetResponseEvent::class);
67+
68+
$request = new Request([], [], ['data' => new \stdClass(), '_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'post', '_api_receive' => false]);
69+
$request->setMethod(Request::METHOD_POST);
70+
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
71+
72+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
73+
$serializerProphecy->deserialize()->shouldNotBeCalled();
74+
75+
$serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
76+
$serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->shouldNotBeCalled();
77+
78+
$listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), self::FORMATS);
79+
$listener->onKernelRequest($eventProphecy->reveal());
80+
}
81+
6482
/**
6583
* @dataProvider methodProvider
6684
*/

tests/EventListener/ReadListenerTest.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,23 @@ public function testNotAnApiPlatformRequest()
3030
$itemDataProvider = $this->prophesize(ItemDataProviderInterface::class);
3131
$itemDataProvider->getItem()->shouldNotBeCalled();
3232

33-
$request = new Request();
33+
$event = $this->prophesize(GetResponseEvent::class);
34+
$event->getRequest()->willReturn(new Request())->shouldBeCalled();
35+
36+
$listener = new ReadListener($collectionDataProvider->reveal(), $itemDataProvider->reveal());
37+
$listener->onKernelRequest($event->reveal());
38+
}
39+
40+
public function testDoNotCallWhenReceiveFlagIsFalse()
41+
{
42+
$collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class);
43+
$collectionDataProvider->getCollection()->shouldNotBeCalled();
44+
45+
$itemDataProvider = $this->prophesize(ItemDataProviderInterface::class);
46+
$itemDataProvider->getItem()->shouldNotBeCalled();
47+
48+
$request = new Request([], [], ['data' => new \stdClass(), '_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'post', '_api_receive' => false]);
49+
$request->setMethod('PUT');
3450

3551
$event = $this->prophesize(GetResponseEvent::class);
3652
$event->getRequest()->willReturn($request)->shouldBeCalled();
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Controller;
13+
14+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CustomActionDummy;
15+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
16+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
17+
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
18+
use Symfony\Component\HttpFoundation\Request;
19+
20+
/**
21+
* @author Kévin Dunglas <[email protected]>
22+
*/
23+
class CustomActionController extends Controller
24+
{
25+
/**
26+
* @Route(
27+
* name="custom_normalization",
28+
* path="/custom/{id}/normalization",
29+
* defaults={"_api_resource_class"=CustomActionDummy::class, "_api_item_operation_name"="custom_normalization"}
30+
* )
31+
* @Method("GET")
32+
*/
33+
public function customNormalizationAction(CustomActionDummy $_data)
34+
{
35+
$_data->setFoo('foo');
36+
37+
return $this->json($_data);
38+
}
39+
40+
/**
41+
* @Route(
42+
* name="custom_denormalization",
43+
* path="/custom/denormalization",
44+
* defaults={
45+
* "_api_resource_class"=CustomActionDummy::class,
46+
* "_api_collection_operation_name"="custom_denormalization",
47+
* "_api_receive"=false
48+
* }
49+
* )
50+
* @Method("POST")
51+
*/
52+
public function customDenormalizationAction(Request $request)
53+
{
54+
if ($request->attributes->has('data')) {
55+
throw new \RuntimeException('The "data" attribute must not be set.');
56+
}
57+
58+
$object = new CustomActionDummy();
59+
$object->setFoo('custom!');
60+
61+
return $object;
62+
}
63+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
13+
14+
use ApiPlatform\Core\Annotation\ApiResource;
15+
use Doctrine\ORM\Mapping as ORM;
16+
17+
/**
18+
* @ORM\Entity
19+
* @ApiResource(itemOperations={
20+
* "get"={"method"="GET"},
21+
* "custom_normalization"={"route_name"="custom_normalization"}
22+
* }, collectionOperations={
23+
* "get"={"method"="GET"},
24+
* "custom_denormalization"={"route_name"="custom_denormalization"}
25+
* })
26+
*
27+
* @author Kévin Dunglas <[email protected]>
28+
*/
29+
class CustomActionDummy
30+
{
31+
/**
32+
* @var int
33+
*
34+
* @ORM\Column(type="integer")
35+
* @ORM\Id
36+
* @ORM\GeneratedValue(strategy="AUTO")
37+
*/
38+
private $id;
39+
40+
/**
41+
* @var string
42+
*
43+
* @ORM\Column
44+
*/
45+
private $foo = '';
46+
47+
public function getId(): int
48+
{
49+
return $this->id;
50+
}
51+
52+
public function getFoo(): string
53+
{
54+
return $this->foo;
55+
}
56+
57+
public function setFoo(string $foo)
58+
{
59+
$this->foo = $foo;
60+
}
61+
}

tests/Fixtures/app/AppKernel.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
1515
use FOS\UserBundle\FOSUserBundle;
1616
use Nelmio\ApiDocBundle\NelmioApiDocBundle;
17+
use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle;
1718
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
1819
use Symfony\Bundle\SecurityBundle\SecurityBundle;
1920
use Symfony\Bundle\TwigBundle\TwigBundle;
@@ -33,6 +34,7 @@ public function registerBundles()
3334
new FrameworkBundle(),
3435
new TwigBundle(),
3536
new DoctrineBundle(),
37+
new SensioFrameworkExtraBundle(),
3638
new ApiPlatformBundle(),
3739
new SecurityBundle(),
3840
new FOSUserBundle(),

tests/Fixtures/app/config/routing.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ relation_embedded.custom_get:
1111
methods: ['GET', 'HEAD']
1212
defaults:
1313
_controller: 'TestBundle:Custom:custom'
14+
15+
controller:
16+
resource: "@TestBundle/Controller"
17+
type: annotation

0 commit comments

Comments
 (0)