├── .gitignore
├── DependencyInjection
├── Configuration.php
└── RequestDataExtension.php
├── Event
└── FinishEvent.php
├── EventListener
└── ControllerListener.php
├── Events.php
├── Exception
└── NotSupportedFormatException.php
├── Extractor
├── Extractor.php
└── ExtractorInterface.php
├── FormatSupportableInterface.php
├── Formats.php
├── LICENSE
├── Mapper
├── Mapper.php
└── MapperInterface.php
├── README.md
├── RequestDataBundle.php
├── Resources
└── config
│ └── services.yml
├── Tests
├── EventListener
│ └── ControllerListenerTest.php
├── Extractor
│ └── ExtractorTest.php
├── Fixtures
│ ├── TestAbstractController.php
│ ├── TestController.php
│ ├── TestFormatSupportableRequestData.php
│ ├── TestRequestData.php
│ └── TestRequestDataController.php
├── Mapper
│ └── MapperTest.php
└── TypeConverter
│ └── TypeConverterTest.php
├── TypeConverter
├── TypeConverter.php
└── TypeConverterInterface.php
└── composer.json
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
3 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class Configuration implements ConfigurationInterface
12 | {
13 | /**
14 | * The DEFAULT_PREFIX defines the default request data namespace prefix.
15 | */
16 | public const DEFAULT_PREFIX = 'App\\RequestData';
17 |
18 | /**
19 | * {@inheritdoc}
20 | */
21 | public function getConfigTreeBuilder()
22 | {
23 | $treeBuilder = new TreeBuilder('request_data');
24 | $rootNode = \method_exists(TreeBuilder::class, 'getRootNode') ? $treeBuilder->getRootNode() : $treeBuilder->root('request_data');
25 |
26 | $rootNode->children()
27 | ->scalarNode('prefix')
28 | ->defaultValue(self::DEFAULT_PREFIX)
29 | ->end();
30 |
31 | return $treeBuilder;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/DependencyInjection/RequestDataExtension.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class RequestDataExtension extends Extension
14 | {
15 | /**
16 | * {@inheritdoc}
17 | */
18 | public function load(array $configs, ContainerBuilder $container)
19 | {
20 | $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
21 | $loader->load('services.yml');
22 |
23 | $configuration = new Configuration();
24 | $config = $this->processConfiguration($configuration, $configs);
25 |
26 | $definition = $container->getDefinition('request_data.controller_listener');
27 | $definition->replaceArgument('$prefix', $config['prefix']);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Event/FinishEvent.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class FinishEvent extends Event
11 | {
12 | /**
13 | * @var object
14 | */
15 | protected $requestData;
16 |
17 | public function __construct(object $requestData)
18 | {
19 | $this->requestData = $requestData;
20 | }
21 |
22 | /**
23 | * @return object
24 | */
25 | public function getRequestData(): object
26 | {
27 | return $this->requestData;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/EventListener/ControllerListener.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class ControllerListener
16 | {
17 | /**
18 | * @var MapperInterface
19 | */
20 | private $mapper;
21 |
22 | /**
23 | * @var EventDispatcherInterface
24 | */
25 | private $dispatcher;
26 |
27 | /**
28 | * @var string
29 | */
30 | private $prefix;
31 |
32 | public function __construct(MapperInterface $mapper, EventDispatcherInterface $dispatcher, string $prefix)
33 | {
34 | $this->mapper = $mapper;
35 | $this->dispatcher = $dispatcher;
36 | $this->prefix = $prefix;
37 | }
38 |
39 | /**
40 | * @param FilterControllerEvent $event
41 | *
42 | * @throws NotSupportedFormatException
43 | * @throws \ReflectionException
44 | */
45 | public function onKernelController(FilterControllerEvent $event)
46 | {
47 | $controller = $event->getController();
48 | if (!\is_array($controller)) {
49 | return;
50 | }
51 |
52 | $controllerClass = new \ReflectionClass($controller[0]);
53 | if ($controllerClass->isAbstract()) {
54 | return;
55 | }
56 |
57 | $parameters = $controllerClass->getMethod($controller[1])->getParameters();
58 | foreach ($parameters as $parameter) {
59 | $class = $parameter->getClass();
60 |
61 | if (null !== $class && 0 === \strpos($class->getName(), $this->prefix)) {
62 | $request = $event->getRequest();
63 |
64 | $object = $class->newInstance();
65 |
66 | $this->mapper->map($request, $object);
67 |
68 | $request->attributes->set($parameter->getName(), $object);
69 |
70 | $this->dispatcher->dispatch(Events::FINISH, new FinishEvent($object));
71 |
72 | break;
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Events.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | final class Events
9 | {
10 | /**
11 | * The FINISH event occurs when request data formation is finished.
12 | *
13 | * @Event("Bilyiv\RequestDataBundle\Event\FinishEvent")
14 | */
15 | public const FINISH = 'request_data.finish';
16 | }
17 |
--------------------------------------------------------------------------------
/Exception/NotSupportedFormatException.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class NotSupportedFormatException extends \Exception
9 | {
10 | }
11 |
--------------------------------------------------------------------------------
/Extractor/Extractor.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class Extractor implements ExtractorInterface
13 | {
14 | /**
15 | * @var TypeConverterInterface
16 | */
17 | private $converter;
18 |
19 | public function __construct(TypeConverterInterface $converter)
20 | {
21 | $this->converter = $converter;
22 | }
23 |
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function extractData(Request $request, string $format)
28 | {
29 | $method = $request->getMethod();
30 |
31 | if (Request::METHOD_GET === $method) {
32 | return $this->converter->convert($request->query->all());
33 | }
34 |
35 | if (Request::METHOD_POST === $method || Request::METHOD_PUT === $method || Request::METHOD_PATCH === $method) {
36 | if (Formats::FORM === $format) {
37 | return $request->files->all() + $request->request->all();
38 | }
39 |
40 | return $request->getContent();
41 | }
42 |
43 | return null;
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public function extractFormat(Request $request): ?string
50 | {
51 | if (Request::METHOD_GET === $request->getMethod()) {
52 | return Formats::FORM;
53 | }
54 |
55 | $format = $request->getFormat($request->headers->get('content-type'));
56 |
57 | if (!\in_array($format, static::getSupportedFormats())) {
58 | return null;
59 | }
60 |
61 | return $format;
62 | }
63 |
64 | /**
65 | * {@inheritdoc}
66 | */
67 | public static function getSupportedFormats(): array
68 | {
69 | return [Formats::FORM, Formats::JSON, Formats::XML];
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Extractor/ExtractorInterface.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | interface ExtractorInterface extends FormatSupportableInterface
12 | {
13 | /**
14 | * Extract data from the request.
15 | *
16 | * @param Request $request
17 | * @param string $format
18 | *
19 | * @return mixed
20 | */
21 | public function extractData(Request $request, string $format);
22 |
23 | /**
24 | * Extract format from the request.
25 | *
26 | * @param Request $request
27 | *
28 | * @return string|null
29 | */
30 | public function extractFormat(Request $request): ?string;
31 | }
32 |
--------------------------------------------------------------------------------
/FormatSupportableInterface.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | interface FormatSupportableInterface
9 | {
10 | /**
11 | * Return supported formats.
12 | *
13 | * @return array
14 | */
15 | public static function getSupportedFormats(): array;
16 | }
17 |
--------------------------------------------------------------------------------
/Formats.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class Formats
9 | {
10 | /**
11 | * The FORM format represents form data content types.
12 | */
13 | public const FORM = 'form';
14 |
15 | /**
16 | * The JSON format represents json content types.
17 | */
18 | public const JSON = 'json';
19 |
20 | /**
21 | * The XML format represents xml content types.
22 | */
23 | public const XML = 'xml';
24 | }
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 bilyiv
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Mapper/Mapper.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class Mapper implements MapperInterface
17 | {
18 | /**
19 | * @var ExtractorInterface
20 | */
21 | private $extractor;
22 |
23 | /**
24 | * @var SerializerInterface
25 | */
26 | private $serializer;
27 |
28 | /**
29 | * @var PropertyAccessorInterface
30 | */
31 | private $propertyAccessor;
32 |
33 | public function __construct(
34 | ExtractorInterface $extractor,
35 | SerializerInterface $serializer,
36 | PropertyAccessorInterface $propertyAccessor
37 | ) {
38 | $this->extractor = $extractor;
39 | $this->serializer = $serializer;
40 | $this->propertyAccessor = $propertyAccessor;
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | public function map(Request $request, object $object): void
47 | {
48 | $format = $this->extractor->extractFormat($request);
49 | $formatSupportable = $object instanceof FormatSupportableInterface;
50 | if (!$format || ($formatSupportable && !\in_array($format, $object::getSupportedFormats()))) {
51 | throw new NotSupportedFormatException();
52 | }
53 |
54 | $data = $this->extractor->extractData($request, $format);
55 | if (!$data) {
56 | return;
57 | }
58 |
59 | if (Formats::FORM === $format && \is_array($data)) {
60 | $this->mapForm($data, $object);
61 | return;
62 | }
63 |
64 | $this->serializer->deserialize($data, \get_class($object), $format, ['object_to_populate' => $object]);
65 | }
66 |
67 | /**
68 | * @param array $data
69 | * @param object $object
70 | */
71 | protected function mapForm(array $data, object $object): void
72 | {
73 | foreach ($data as $propertyPath => $propertyValue) {
74 | if ($this->propertyAccessor->isWritable($object, $propertyPath)) {
75 | $this->propertyAccessor->setValue($object, $propertyPath, $propertyValue);
76 | }
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Mapper/MapperInterface.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | interface MapperInterface
12 | {
13 | /**
14 | * Map request to certain object.
15 | *
16 | * @param Request $request
17 | * @param object $object
18 | *
19 | * @throws NotSupportedFormatException
20 | */
21 | public function map(Request $request, object $object): void;
22 | }
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Request data bundle
2 |
3 | This bundle allows you to represent request data in a structured and useful way by creating request data classes.
4 |
5 | **Features**:
6 |
7 | * Detecting how to extract data depends on request method and `Content-Type` header.
8 | * Representing and normalizing query parameters for the `GET` request method.
9 | * Representing `form`, `json`, `xml` request body for the `POST`, `PUT`, `PATCH` request methods.
10 | * Defining supported formats and throwing exception if the request format is unsupported.
11 | * Dispatching the finish event when request data is ready.
12 |
13 | ## Installation
14 |
15 | Run the following command using [Composer](http://packagist.org):
16 |
17 | ```sh
18 | composer require bilyiv/request-data-bundle
19 | ```
20 |
21 | ## Configuration
22 |
23 | The default configuration is the following:
24 |
25 | ```yaml
26 | request_data:
27 | prefix: App\RequestData
28 | ```
29 |
30 | ## Usage
31 |
32 | #### Create a request data class
33 |
34 | ```php
35 | namespace App\RequestData;
36 |
37 | class PostRequestData implements FormatSupportableInterface
38 | {
39 | public const DEFAULT_AUTHOR = 'none';
40 |
41 | /**
42 | * @var string
43 | */
44 | public $title;
45 |
46 | /**
47 | * @var string
48 | */
49 | public $author = self::DEFAULT_AUTHOR;
50 |
51 | /**
52 | * {@inheritdoc}
53 | */
54 | public static function getSupportedFormats(): array
55 | {
56 | return [Formats::FORM, Formats::JSON, Formats::XML];
57 | }
58 | }
59 | ```
60 |
61 | #### Use it in your controller
62 |
63 | ```php
64 | namespace App\Controller;
65 |
66 | class PostController extends AbstractController
67 | {
68 | /**
69 | * @Route("/", name="action")
70 | */
71 | public function action(PostRequestData $data)
72 | {
73 | return new JsonResponse($data);
74 | }
75 | }
76 | ```
77 |
78 | #### Make requests
79 |
80 | All the following requests will return the same json response:
81 |
82 | ```json
83 | {
84 | "title": "Hamlet",
85 | "author": "William Shakespeare"
86 | }
87 | ```
88 |
89 | `GET` request:
90 |
91 | ```bash
92 | curl -X GET 'https://example.com?title=Hamlet&author=William+Shakespeare'
93 | ```
94 |
95 | `POST` form request:
96 |
97 | ```bash
98 | curl -X POST 'https://example.com' \
99 | -H 'Content-Type: application/x-www-form-urlencoded' \
100 | -d 'title=Hamlet&author=William+Shakespeare'
101 | ```
102 |
103 | `POST` json request:
104 |
105 | ```bash
106 | curl -X POST 'https://example.com' \
107 | -H 'Content-Type: application/json' \
108 | -d '{"title":"Hamlet","author":"William Shakespeare"}'
109 | ```
110 |
111 | `POST` xml request:
112 |
113 | ```bash
114 | curl -X POST 'https://example.com' \
115 | -H 'Content-Type: application/xml' \
116 | -d 'HamletWilliam Shakespeare'
117 | ```
118 |
119 | `POST` csv request throws an exception because of unsupported format:
120 |
121 | ```bash
122 | curl -X POST 'https://example.com' \
123 | -H 'Content-Type: application/csv' \
124 | -d 'Hamlet,William Shakespeare'
125 | ```
126 |
127 | ## License
128 |
129 | This bundle is released under the MIT license. See the included [LICENSE](LICENSE) file for more information.
130 |
--------------------------------------------------------------------------------
/RequestDataBundle.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class RequestDataBundle extends Bundle
11 | {
12 | }
13 |
--------------------------------------------------------------------------------
/Resources/config/services.yml:
--------------------------------------------------------------------------------
1 | services:
2 | _defaults:
3 | autowire: true
4 | autoconfigure: true
5 |
6 | request_data.extractor:
7 | class: 'Bilyiv\RequestDataBundle\Extractor\Extractor'
8 |
9 | Bilyiv\RequestDataBundle\Extractor\ExtractorInterface: '@request_data.extractor'
10 |
11 | request_data.mapper:
12 | class: 'Bilyiv\RequestDataBundle\Mapper\Mapper'
13 |
14 | Bilyiv\RequestDataBundle\Mapper\MapperInterface: '@request_data.mapper'
15 |
16 | request_data.type_converter:
17 | class: 'Bilyiv\RequestDataBundle\TypeConverter\TypeConverter'
18 |
19 | Bilyiv\RequestDataBundle\TypeConverter\TypeConverterInterface: '@request_data.type_converter'
20 |
21 | request_data.controller_listener:
22 | class: 'Bilyiv\RequestDataBundle\EventListener\ControllerListener'
23 | arguments:
24 | $prefix: ''
25 | tags:
26 | - {name: 'kernel.event_listener', event: 'kernel.controller'}
--------------------------------------------------------------------------------
/Tests/EventListener/ControllerListenerTest.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | class ControllerListenerTest extends TestCase
20 | {
21 | /**
22 | * @var ControllerListener
23 | */
24 | private $controllerListener;
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | public function setUp()
30 | {
31 | $mapper = $this->getMockBuilder(MapperInterface::class)
32 | ->disableOriginalConstructor()
33 | ->getMock();
34 |
35 | $mapper
36 | ->method('map')
37 | ->willReturnCallback(function (Request $request, object $object) {
38 | $object->foo = 'bar';
39 | });
40 |
41 | $dispatcher = $this->getMockBuilder(EventDispatcherInterface::class)
42 | ->disableOriginalConstructor()
43 | ->getMock();
44 |
45 | $this->controllerListener = new ControllerListener(
46 | $mapper,
47 | $dispatcher,
48 | 'Bilyiv\RequestDataBundle\Tests\Fixtures'
49 | );
50 | }
51 |
52 | /**
53 | * Tests if listener do nothing when there is wrong controller.
54 | */
55 | public function testOnKernelControllerWithWrongController()
56 | {
57 | $filterControllerEvent = $this->getMockBuilder(FilterControllerEvent::class)
58 | ->disableOriginalConstructor()
59 | ->getMock();
60 |
61 | $filterControllerEvent
62 | ->expects($this->once())
63 | ->method('getController')
64 | ->willReturn(null);
65 |
66 | $filterControllerEvent
67 | ->expects($this->never())
68 | ->method('getRequest');
69 |
70 | $result = $this->controllerListener->onKernelController($filterControllerEvent);
71 |
72 | $this->assertNull($result);
73 | }
74 |
75 | /**
76 | * Tests if listener do nothing when there is an abstract controller.
77 | */
78 | public function testOnKernelControllerWithAbstractController()
79 | {
80 | $filterControllerEvent = $this->getMockBuilder(FilterControllerEvent::class)
81 | ->disableOriginalConstructor()
82 | ->getMock();
83 |
84 | $filterControllerEvent
85 | ->expects($this->once())
86 | ->method('getController')
87 | ->willReturn([TestAbstractController::class]);
88 |
89 | $filterControllerEvent
90 | ->expects($this->never())
91 | ->method('getRequest');
92 |
93 | $result = $this->controllerListener->onKernelController($filterControllerEvent);
94 |
95 | $this->assertNull($result);
96 | }
97 |
98 | /**
99 | * Tests if listener do nothing when there is a controller without injected request data class.
100 | */
101 | public function testOnKernelControllerWithoutInjectedRequestData()
102 | {
103 | $filterControllerEvent = $this->getMockBuilder(FilterControllerEvent::class)
104 | ->disableOriginalConstructor()
105 | ->getMock();
106 |
107 | $filterControllerEvent
108 | ->expects($this->once())
109 | ->method('getController')
110 | ->willReturn([TestController::class, 'index']);
111 |
112 | $filterControllerEvent
113 | ->expects($this->never())
114 | ->method('getRequest');
115 |
116 | $result = $this->controllerListener->onKernelController($filterControllerEvent);
117 |
118 | $this->assertNull($result);
119 | }
120 |
121 | /**
122 | * Tests if listener do nothing when there is a controller with injected request data class.
123 | */
124 | public function testOnKernelControllerWithInjectedRequestData()
125 | {
126 | $request = new Request();
127 |
128 | $filterControllerEvent = $this->getMockBuilder(FilterControllerEvent::class)
129 | ->disableOriginalConstructor()
130 | ->getMock();
131 |
132 | $filterControllerEvent
133 | ->expects($this->once())
134 | ->method('getController')
135 | ->willReturn([TestRequestDataController::class, 'index']);
136 |
137 | $filterControllerEvent
138 | ->expects($this->once())
139 | ->method('getRequest')
140 | ->willReturn($request);
141 |
142 | $result = $this->controllerListener->onKernelController($filterControllerEvent);
143 |
144 | $this->assertNull($result);
145 | $this->assertEquals(1, $request->attributes->count());
146 | $this->assertInstanceOf(TestRequestData::class, $request->attributes->get('data'));
147 | $this->assertEquals('bar', $request->attributes->get('data')->foo);
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/Tests/Extractor/ExtractorTest.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class ExtractorTest extends TestCase
16 | {
17 | /**
18 | * @var ExtractorInterface
19 | */
20 | private $extractor;
21 |
22 | /**
23 | * {@inheritdoc}
24 | */
25 | public function setUp()
26 | {
27 | $converter = $this->getMockBuilder(TypeConverterInterface::class)
28 | ->disableOriginalConstructor()
29 | ->getMock();
30 |
31 | $converter
32 | ->method('convert')
33 | ->willReturn(['get' => 'request']);
34 |
35 | $this->extractor = new Extractor($converter);
36 | }
37 |
38 | /**
39 | * Test if extractor implements necessary interface.
40 | */
41 | public function testInterface()
42 | {
43 | $this->assertInstanceOf(ExtractorInterface::class, $this->extractor);
44 | }
45 |
46 | /**
47 | * Test if extractor returns form data from GET request.
48 | */
49 | public function testExtractFormDataFromGetRequest()
50 | {
51 | $request = Request::create('/', Request::METHOD_GET, ['get' => 'request']);
52 |
53 | $this->assertEquals(['get' => 'request'], $this->extractor->extractData($request, Formats::FORM));
54 | }
55 |
56 | /**
57 | * Test if extractor returns json data from POST request.
58 | */
59 | public function testExtractJsonDataFromPostRequest()
60 | {
61 | $request = Request::create('/', Request::METHOD_POST, [], [], [], [], '{"post":"request"}');
62 | $request->headers->set('content-type', 'application/json');
63 |
64 | $this->assertEquals('{"post":"request"}', $this->extractor->extractData($request, Formats::JSON));
65 | }
66 |
67 | /**
68 | * Test if extractor returns json data from PUT request.
69 | */
70 | public function testExtractJsonDataFromPutRequest()
71 | {
72 | $request = Request::create('/', Request::METHOD_POST, [], [], [], [], '{"put":"request"}');
73 | $request->headers->set('content-type', 'application/json');
74 |
75 | $this->assertEquals('{"put":"request"}', $this->extractor->extractData($request, Formats::JSON));
76 | }
77 |
78 | /**
79 | * Test if extractor returns json data from PATCH request.
80 | */
81 | public function testExtractJsonDataFromPatchRequest()
82 | {
83 | $request = Request::create('/', Request::METHOD_PATCH, [], [], [], [], '{"patch":"request"}');
84 | $request->headers->set('content-type', 'application/json');
85 |
86 | $this->assertEquals('{"patch":"request"}', $this->extractor->extractData($request, Formats::JSON));
87 | }
88 |
89 | /**
90 | * Test if extractor returns xml data from POST request.
91 | */
92 | public function testExtractXmlDataFromPostRequest()
93 | {
94 | $request = Request::create('/', Request::METHOD_POST, [], [], [], [], 'request');
95 | $request->headers->set('content-type', 'application/xml');
96 |
97 | $this->assertEquals('request', $this->extractor->extractData($request, Formats::XML));
98 | }
99 |
100 | /**
101 | * Test if extractor returns xml data from PUT request.
102 | */
103 | public function testExtractXmlDataFromPutRequest()
104 | {
105 | $request = Request::create('/', Request::METHOD_POST, [], [], [], [], 'request');
106 | $request->headers->set('content-type', 'application/xml');
107 |
108 | $this->assertEquals('request', $this->extractor->extractData($request, Formats::XML));
109 | }
110 |
111 | /**
112 | * Test if extractor returns xml data from PATCH request.
113 | */
114 | public function testExtractXmlDataFromPatchRequest()
115 | {
116 | $request = Request::create('/', Request::METHOD_PATCH, [], [], [], [], 'request');
117 | $request->headers->set('content-type', 'application/xml');
118 |
119 | $this->assertEquals('request', $this->extractor->extractData($request, Formats::XML));
120 | }
121 |
122 | /**
123 | * Test if extractor returns form format from GET request.
124 | */
125 | public function testExtractFormFormatFromGetRequest()
126 | {
127 | $request = Request::create('/', Request::METHOD_GET);
128 |
129 | $this->assertEquals(Formats::FORM, $this->extractor->extractFormat($request));
130 | }
131 |
132 | /**
133 | * Test if extractor returns form format from POST request.
134 | */
135 | public function testExtractFormFormatFromPostRequest()
136 | {
137 | $request = Request::create('/', Request::METHOD_POST);
138 |
139 | $this->assertEquals(Formats::FORM, $this->extractor->extractFormat($request));
140 | }
141 |
142 | /**
143 | * Test if extractor returns form format from PUT request.
144 | */
145 | public function testExtractFormFormatFromPutRequest()
146 | {
147 | $request = Request::create('/', Request::METHOD_PUT);
148 |
149 | $this->assertEquals(Formats::FORM, $this->extractor->extractFormat($request));
150 | }
151 |
152 | /**
153 | * Test if extractor returns json format from POST request.
154 | */
155 | public function testExtractJsonFormatFromPostRequest()
156 | {
157 | $request = Request::create('/', Request::METHOD_POST);
158 | $request->headers->set('content-type', 'application/json');
159 |
160 | $this->assertEquals(Formats::JSON, $this->extractor->extractFormat($request));
161 | }
162 |
163 | /**
164 | * Test if extractor returns json format from PUT request.
165 | */
166 | public function testExtractJsonFormatFromPutRequest()
167 | {
168 | $request = Request::create('/', Request::METHOD_PUT);
169 | $request->headers->set('content-type', 'application/json');
170 |
171 | $this->assertEquals(Formats::JSON, $this->extractor->extractFormat($request));
172 | }
173 |
174 | /**
175 | * Test if extractor returns json format from PATCH request.
176 | */
177 | public function testExtractJsonFormatFromPatchRequest()
178 | {
179 | $request = Request::create('/', Request::METHOD_PATCH);
180 | $request->headers->set('content-type', 'application/json');
181 |
182 | $this->assertEquals(Formats::JSON, $this->extractor->extractFormat($request));
183 | }
184 |
185 | /**
186 | * Test if extractor returns xml format from POST request.
187 | */
188 | public function testExtractXmlFormatFromPostRequest()
189 | {
190 | $request = Request::create('/', Request::METHOD_POST);
191 | $request->headers->set('content-type', 'application/xml');
192 |
193 | $this->assertEquals(Formats::XML, $this->extractor->extractFormat($request));
194 | }
195 |
196 | /**
197 | * Test if extractor returns xml format from PUT request.
198 | */
199 | public function testExtractXmlFormatFromPutRequest()
200 | {
201 | $request = Request::create('/', Request::METHOD_PUT);
202 | $request->headers->set('content-type', 'application/xml');
203 |
204 | $this->assertEquals(Formats::XML, $this->extractor->extractFormat($request));
205 | }
206 |
207 | /**
208 | * Test if extractor returns xml format from PATCH request.
209 | */
210 | public function testExtractXmlFormatFromPatchRequest()
211 | {
212 | $request = Request::create('/', Request::METHOD_PATCH);
213 | $request->headers->set('content-type', 'application/xml');
214 |
215 | $this->assertEquals(Formats::XML, $this->extractor->extractFormat($request));
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/Tests/Fixtures/TestAbstractController.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | abstract class TestAbstractController
9 | {
10 | }
11 |
--------------------------------------------------------------------------------
/Tests/Fixtures/TestController.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class TestController extends TestAbstractController
11 | {
12 | public function index(Request $request)
13 | {
14 | return null;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/Fixtures/TestFormatSupportableRequestData.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class TestFormatSupportableRequestData implements FormatSupportableInterface
12 | {
13 | /**
14 | * @var string|null
15 | */
16 | public $foo;
17 |
18 | /**
19 | * {@inheritdoc}
20 | */
21 | public static function getSupportedFormats(): array
22 | {
23 | return [Formats::JSON];
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/Fixtures/TestRequestData.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class TestRequestData
9 | {
10 | /**
11 | * @var string|null
12 | */
13 | public $foo;
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/Fixtures/TestRequestDataController.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class TestRequestDataController extends TestAbstractController
9 | {
10 | public function index(TestRequestData $data)
11 | {
12 | return $data;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/Mapper/MapperTest.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | class MapperTest extends TestCase
22 | {
23 | /**
24 | * @var ExtractorInterface|MockObject
25 | */
26 | private $extractor;
27 |
28 | /**
29 | * @var SerializerInterface|MockObject
30 | */
31 | private $serializer;
32 |
33 | /**
34 | * @var MapperInterface|MockObject
35 | */
36 | private $mapper;
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | public function setUp()
42 | {
43 | $this->extractor = $this->getMockBuilder(ExtractorInterface::class)
44 | ->disableOriginalConstructor()
45 | ->getMock();
46 |
47 | $this->serializer = $this->getMockBuilder(SerializerInterface::class)
48 | ->disableOriginalConstructor()
49 | ->getMock();
50 |
51 | $this->mapper = new Mapper($this->extractor, $this->serializer, new PropertyAccessor());
52 | }
53 |
54 | /**
55 | * Test if mapper implements necessary interfaces.
56 | */
57 | public function testInterface()
58 | {
59 | $this->assertInstanceOf(MapperInterface::class, $this->mapper);
60 | }
61 |
62 | /**
63 | * Tests if mapper throws unsupported format exception.
64 | */
65 | public function testUnsupportedRequestFormatException()
66 | {
67 | $this->extractor
68 | ->method('extractFormat')
69 | ->willReturn(Formats::FORM);
70 |
71 | $this->expectException(NotSupportedFormatException::class);
72 |
73 | $this->mapper->map(new Request(), new TestFormatSupportableRequestData());
74 | }
75 |
76 | /**
77 | * Test if mapper handles form requests correctly.
78 | */
79 | public function testFormRequestMapping()
80 | {
81 | $this->extractor
82 | ->method('extractFormat')
83 | ->willReturn(Formats::FORM);
84 |
85 | $this->extractor
86 | ->method('extractData')
87 | ->willReturn(['foo' => 'bar']);
88 |
89 | $this->serializer
90 | ->expects($this->never())
91 | ->method('deserialize')
92 | ->willReturn(null);
93 |
94 | $requestData = new TestRequestData();
95 |
96 | $this->mapper->map(new Request(), $requestData);
97 |
98 | $this->assertEquals('bar', $requestData->foo);
99 | }
100 |
101 | /**
102 | * Test if mapper handles not form requests correctly.
103 | */
104 | public function testNotFormRequestMapping()
105 | {
106 | $this->extractor
107 | ->method('extractFormat')
108 | ->willReturn(Formats::JSON);
109 |
110 | $this->extractor
111 | ->method('extractData')
112 | ->willReturn('{"foo":"bar"}');
113 |
114 | $this->serializer
115 | ->expects($this->once())
116 | ->method('deserialize')
117 | ->willReturn(null);
118 |
119 | $this->mapper->map(new Request(), new TestRequestData());
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/Tests/TypeConverter/TypeConverterTest.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class TypeConverterTest extends TestCase
13 | {
14 | /**
15 | * @var TypeConverter
16 | */
17 | private $converter;
18 |
19 | /**
20 | * {@inheritdoc}
21 | */
22 | public function setUp()
23 | {
24 | $this->converter = new TypeConverter();
25 | }
26 |
27 | /**
28 | * Test if type converter implements necessary interface.
29 | */
30 | public function testInterface()
31 | {
32 | $this->assertInstanceOf(TypeConverterInterface::class, $this->converter);
33 | }
34 |
35 | /**
36 | * Test if type converter converts scalar types correctly.
37 | */
38 | public function testConvertScalars()
39 | {
40 | $this->assertNull($this->converter->convert(''));
41 | $this->assertNull($this->converter->convert('null'));
42 | $this->assertTrue($this->converter->convert('true'));
43 | $this->assertFalse($this->converter->convert('false'));
44 | $this->assertIsInt($this->converter->convert('10'));
45 | $this->assertIsFloat($this->converter->convert('10.1'));
46 | }
47 |
48 | /**
49 | * Test if type converter converts array correctly.
50 | */
51 | public function testConvertArray()
52 | {
53 | $this->assertEquals([null, 10.1], $this->converter->convert(['', '10.1']));
54 | $this->assertEquals([10 => [true]], $this->converter->convert([10 => ['true']]));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/TypeConverter/TypeConverter.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class TypeConverter implements TypeConverterInterface
9 | {
10 | /**
11 | * {@inheritdoc}
12 | */
13 | public function convert($value)
14 | {
15 | if (\is_string($value)) {
16 | $value = $this->convertString($value);
17 | }
18 |
19 | if (\is_array($value)) {
20 | $value = $this->convertArray($value);
21 | }
22 |
23 | return $value;
24 | }
25 |
26 | /**
27 | * @param array $values
28 | *
29 | * @return array
30 | */
31 | protected function convertArray(array $values)
32 | {
33 | foreach ($values as &$value) {
34 | $value = $this->convert($value);
35 | }
36 |
37 | return $values;
38 | }
39 |
40 | /**
41 | * @param string $value
42 | *
43 | * @return mixed
44 | */
45 | protected function convertString(string $value)
46 | {
47 | if ('' === $value || 'null' === $value) {
48 | return null;
49 | }
50 | if ('true' === $value) {
51 | return true;
52 | }
53 | if ('false' === $value) {
54 | return false;
55 | }
56 | if (\preg_match('/^-?\d+$/', $value)) {
57 | return (int) $value;
58 | }
59 | if (\preg_match('/^-?\d+(\.\d+)?$/', $value)) {
60 | return (float) $value;
61 | }
62 |
63 | return $value;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/TypeConverter/TypeConverterInterface.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | interface TypeConverterInterface
9 | {
10 | /**
11 | * @param $value
12 | *
13 | * @return mixed
14 | */
15 | public function convert($value);
16 | }
17 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bilyiv/request-data-bundle",
3 | "type": "symfony-bundle",
4 | "description": "Represents request data in a structured and useful way.",
5 | "keywords": ["request", "data", "api", "rest", "json"],
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Vladyslav Bilyi",
10 | "email": "beliyvladislav@gmail.com"
11 | }
12 | ],
13 | "prefer-stable": true,
14 | "require": {
15 | "php": "^7.2",
16 | "symfony/config": "~3.4|~4.0",
17 | "symfony/dependency-injection": "~3.4|~4.0",
18 | "symfony/http-kernel": "~3.4|~4.0",
19 | "symfony/serializer": "~3.4|~4.0",
20 | "symfony/event-dispatcher": "~3.4|~4.0",
21 | "symfony/property-access": "~3.4|~4.0"
22 | },
23 | "require-dev": {
24 | "phpunit/phpunit": "^7"
25 | },
26 | "autoload": {
27 | "psr-4": {
28 | "Bilyiv\\RequestDataBundle\\": ""
29 | },
30 | "exclude-from-classmap": [
31 | "/Tests/"
32 | ]
33 | },
34 | "extra": {
35 | "branch-alias": {
36 | "dev-master": "1.0.x-dev"
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------