├── .gitignore
├── .scrutinizer.yml
├── .travis.yml
├── DependencyInjection
├── Configuration.php
└── MediaMonksRestApiExtension.php
├── LICENSE
├── MediaMonksRestApiBundle.php
├── README.md
├── Resources
├── config
│ └── services.xml
└── doc
│ ├── 0-requirements.rst
│ ├── 1-setting_up_the_bundle.rst
│ ├── 2-configuring_the_bundle.rst
│ ├── 3-using_the_bundle.rst
│ └── configuration_reference.rst
├── Tests
├── DependencyInjection
│ └── MediaMonksRestApiExtensionTest.php
├── Functional
│ ├── ApiControllerTest.php
│ ├── config
│ │ ├── bundles.php
│ │ ├── packages
│ │ │ ├── framework.yaml
│ │ │ ├── twig.yaml
│ │ │ └── validator.yaml
│ │ ├── routes.yaml
│ │ ├── routes
│ │ │ └── annotations.yaml
│ │ └── services.yaml
│ ├── src
│ │ ├── Controller
│ │ │ └── ApiController.php
│ │ ├── Form
│ │ │ └── Type
│ │ │ │ └── TestType.php
│ │ └── Kernel.php
│ └── var
│ │ └── .gitignore
├── MediaMonksRestApiBundleTest.php
└── bootstrap.php
├── composer.json
└── phpunit.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /composer.lock
3 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | filter:
2 | excluded_paths: [Tests/*]
3 |
4 | checks:
5 | php:
6 | code_rating: true
7 | duplication: true
8 |
9 | tools:
10 | external_code_coverage: true
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | sudo: false
4 |
5 | cache: false
6 |
7 | matrix:
8 | include:
9 | - php: 7.1
10 | env: SYMFONY_VERSION=4.4.*@dev
11 | - php: 7.2
12 | env: SYMFONY_VERSION=4.4.*@dev
13 | - php: 7.3
14 | env: SYMFONY_VERSION=4.4.*@dev
15 | - php: 7.3
16 | env: SYMFONY_VERSION=5.0.*@dev
17 |
18 | before_install:
19 | - composer self-update
20 | - if [ "$SYMFONY_VERSION" != "" ]; then composer require "symfony/symfony:${SYMFONY_VERSION}" --no-update; fi;
21 | - if [ "$TRAVIS_PHP_VERSION" == "7.2" ] && [ "$SYMFONY_VERSION" == "4.4.*" ]; then composer require "codeclimate/php-test-reporter:dev-master@dev" --no-update; fi;
22 | - rm -Rf Tests/Functional/var/cache/test
23 |
24 | install:
25 | - composer update --prefer-source $COMPOSER_FLAGS
26 |
27 | script:
28 | - if [ "$TRAVIS_PHP_VERSION" == "7.2" ] && [ "$SYMFONY_VERSION" == "4.4.*" ]; then vendor/bin/phpunit --coverage-clover=coverage.clover; else vendor/bin/phpunit; fi;
29 |
30 | after_script:
31 | - if [ "$TRAVIS_PHP_VERSION" == "7.2" ] && [ "$SYMFONY_VERSION" == "4.4.*" ]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi;
32 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | getRootNode();
20 |
21 | $this->addDebugNode($rootNode);
22 | $this->addRequestMatcherNode($rootNode);
23 | $this->addSerializer($rootNode);
24 | $this->addPostMessageOriginNode($rootNode);
25 | $this->addResponseModel($rootNode);
26 |
27 | return $treeBuilder;
28 | }
29 |
30 | /**
31 | * @param ArrayNodeDefinition $node
32 | */
33 | private function addDebugNode(ArrayNodeDefinition $node)
34 | {
35 | $node->children()
36 | ->scalarNode('debug')
37 | ->defaultNull()
38 | ->end();
39 | }
40 |
41 | /**
42 | * @param ArrayNodeDefinition $node
43 | */
44 | private function addRequestMatcherNode(ArrayNodeDefinition $node)
45 | {
46 | $node->children()
47 | ->arrayNode('request_matcher')
48 | ->addDefaultsIfNotSet()
49 | ->children()
50 | ->scalarNode('path')
51 | ->end()
52 | ->arrayNode('whitelist')
53 | ->defaultValue(['~^/api$~', '~^/api/~'])
54 | ->prototype('scalar')
55 | ->end()
56 | ->end()
57 | ->arrayNode('blacklist')
58 | ->defaultValue(['~^/api/doc~'])
59 | ->prototype('scalar')
60 | ->end()
61 | ->end()
62 | ->end()
63 | ->end();
64 | }
65 |
66 | /**
67 | * @param ArrayNodeDefinition $node
68 | */
69 | private function addSerializer(ArrayNodeDefinition $node)
70 | {
71 | $node->children()
72 | ->scalarNode('serializer')
73 | ->defaultValue('json')
74 | ->end();
75 | }
76 |
77 | /**
78 | * @param ArrayNodeDefinition $node
79 | */
80 | private function addPostMessageOriginNode(ArrayNodeDefinition $node)
81 | {
82 | $node->children()
83 | ->scalarNode('post_message_origin')
84 | ->defaultNull()
85 | ->end();
86 | }
87 |
88 | /**
89 | * @param ArrayNodeDefinition $node
90 | */
91 | private function addResponseModel(ArrayNodeDefinition $node)
92 | {
93 | $node->children()
94 | ->scalarNode('response_model')
95 | ->defaultNull()
96 | ->end();
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/DependencyInjection/MediaMonksRestApiExtension.php:
--------------------------------------------------------------------------------
1 | processConfiguration(new Configuration(), $configs);
25 |
26 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
27 | $loader->load('services.xml');
28 |
29 | if (!empty($config['request_matcher']['path'])) {
30 | $this->usePathRequestMatcher($container, $config);
31 | }
32 | elseif (!empty($config['request_matcher']['whitelist'])) {
33 | $this->useRegexRequestMatcher($container, $config);
34 | }
35 |
36 | $container->getDefinition('mediamonks_rest_api.response_transformer')
37 | ->replaceArgument(
38 | 2,
39 | [
40 | 'debug' => $this->getDebug($config, $container),
41 | 'post_message_origin' => $config['post_message_origin'],
42 | ]
43 | );
44 |
45 | if (!empty($config['response_model'])) {
46 | $this->replaceResponseModel($container, $config);
47 | }
48 |
49 | $this->loadSerializer($container, $config);
50 | }
51 |
52 | /**
53 | * @param ContainerBuilder $container
54 | * @param array $config
55 | */
56 | protected function usePathRequestMatcher(ContainerBuilder $container, array $config)
57 | {
58 | $container->getDefinition('mediamonks_rest_api.path_request_matcher')
59 | ->replaceArgument(0, $config['request_matcher']['path']);
60 |
61 | $container->getDefinition('mediamonks_rest_api.rest_api_event_subscriber')
62 | ->replaceArgument(0, new Reference('mediamonks_rest_api.path_request_matcher'));
63 | }
64 |
65 | /**
66 | * @param ContainerBuilder $container
67 | * @param array $config
68 | */
69 | protected function useRegexRequestMatcher(ContainerBuilder $container, array $config)
70 | {
71 | $container->getDefinition('mediamonks_rest_api.regex_request_matcher')
72 | ->replaceArgument(0, $config['request_matcher']['whitelist']);
73 | $container->getDefinition('mediamonks_rest_api.regex_request_matcher')
74 | ->replaceArgument(1, $config['request_matcher']['blacklist']);
75 | }
76 |
77 | /**
78 | * @param ContainerBuilder $container
79 | * @param array $config
80 | */
81 | protected function loadSerializer(ContainerBuilder $container, array $config)
82 | {
83 | if (!$container->has($config['serializer'])) {
84 | $config['serializer'] = 'mediamonks_rest_api.serializer.'.$config['serializer'];
85 | }
86 |
87 | $container->getDefinition('mediamonks_rest_api.request_transformer')
88 | ->replaceArgument(0, new Reference($config['serializer']));
89 | $container->getDefinition('mediamonks_rest_api.response_transformer')
90 | ->replaceArgument(0, new Reference($config['serializer']));
91 | }
92 |
93 | /**
94 | * @param ContainerBuilder $container
95 | * @param array $config
96 | */
97 | protected function replaceResponseModel(ContainerBuilder $container, array $config)
98 | {
99 | $container->getDefinition('mediamonks_rest_api.response_model_factory')
100 | ->replaceArgument(0, new Reference($config['response_model']));
101 | }
102 |
103 | /**
104 | * @return string
105 | */
106 | public function getAlias()
107 | {
108 | return 'mediamonks_rest_api';
109 | }
110 |
111 | /**
112 | * @param array $config
113 | * @param ContainerBuilder $container
114 | * @return bool
115 | */
116 | public function getDebug(array $config, ContainerBuilder $container)
117 | {
118 | if (isset($config['debug'])) {
119 | return $config['debug'];
120 | }
121 | if ($container->hasParameter('kernel.debug')) {
122 | return $container->getParameter('kernel.debug');
123 | }
124 |
125 | return false;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 MediaMonks
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 |
--------------------------------------------------------------------------------
/MediaMonksRestApiBundle.php:
--------------------------------------------------------------------------------
1 | extension) {
16 | $this->extension = new MediaMonksRestApiExtension();
17 | }
18 |
19 | return $this->extension;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/mediamonks/symfony-rest-api-bundle)
2 | [](https://scrutinizer-ci.com/g/mediamonks/symfony-rest-api-bundle/?branch=master)
3 | [](https://scrutinizer-ci.com/g/mediamonks/symfony-rest-api-bundle/?branch=master)
4 | [](https://packagist.org/packages/mediamonks/rest-api-bundle)
5 | [](https://packagist.org/packages/mediamonks/rest-api-bundle)
6 | [](https://packagist.org/packages/mediamonks/rest-api-bundle)
7 | [](https://insight.sensiolabs.com/projects/c42e43fd-9c7b-47e1-8264-3a98961e9236)
8 | [](https://packagist.org/packages/mediamonks/rest-api-bundle)
9 |
10 | # MediaMonks Symfony Rest API Bundle
11 |
12 | This bundle provides tools to implement a Rest API by using the [MediaMonks Rest Api](https://github.com/mediamonks/php-rest-api) library.
13 |
14 | ## Highlights
15 |
16 | - Easiest Rest API bundle to use
17 | - Supports all library options
18 |
19 | The highlights of the library itself can be found in the libraries [readme](https://github.com/mediamonks/php-rest-api).
20 |
21 | ## Documentation
22 |
23 | Please refer to the files in the [/Resources/doc](/Resources/doc) folder.
24 |
25 | ## Requirements
26 |
27 | - PHP ^5.6, ^7.0
28 | - Symfony ^3.0, ^4.0
29 |
30 | To use the library.
31 |
32 | ## Security
33 |
34 | If you discover any security related issues, please email devmonk@mediamonks.com instead of using the issue tracker.
35 |
36 | ## License
37 |
38 | The MIT License (MIT). Please see [License File](LICENSE) for more information.
39 |
--------------------------------------------------------------------------------
/Resources/config/services.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriber
9 | MediaMonks\RestApi\Request\RegexRequestMatcher
10 | MediaMonks\RestApi\Request\PathRequestMatcher
11 | MediaMonks\RestApi\Request\RequestTransformer
12 | MediaMonks\RestApi\Response\ResponseTransformer
13 | MediaMonks\RestApi\Serializer\JMSSerializer
14 | MediaMonks\RestApi\Serializer\JsonSerializer
15 | MediaMonks\RestApi\Serializer\MsgpackSerializer
16 | MediaMonks\RestApi\Model\ResponseModel
17 | MediaMonks\RestApi\Model\ResponseModelFactory
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/Resources/doc/0-requirements.rst:
--------------------------------------------------------------------------------
1 | Step 0: Requirements
2 | ====================
3 |
4 | This bundle needs at least PHP 5.4 and `Symfony Framework`_ 2.7+ to work correctly.
5 |
6 | .. _Symfony Framework: https://github.com/symfony/symfony
7 |
--------------------------------------------------------------------------------
/Resources/doc/1-setting_up_the_bundle.rst:
--------------------------------------------------------------------------------
1 | Step 1: Setting up the bundle
2 | =============================
3 |
4 | A) Download the Bundle
5 | ----------------------
6 |
7 | Open a command console, enter your project directory and execute the
8 | following command to download the latest stable version of this bundle:
9 |
10 | .. code-block:: bash
11 |
12 | $ composer require mediamonks/rest-api-bundle ~3.0
13 |
14 | This command requires you to have Composer installed globally, as explained
15 | in the `installation chapter`_ of the Composer documentation.
16 |
17 | B) Enable the Bundle
18 | --------------------
19 |
20 | Then, enable the bundle by adding the following line in the ``app/AppKernel.php``
21 | file of your project:
22 |
23 | .. code-block:: php
24 |
25 | // app/AppKernel.php
26 | class AppKernel extends Kernel
27 | {
28 | public function registerBundles()
29 | {
30 | $bundles = [
31 | // ...
32 | new MediaMonks\RestApiBundle\MediaMonksRestApiBundle(),
33 | ];
34 |
35 | // ...
36 | }
37 | }
38 |
39 | .. _`installation chapter`: https://getcomposer.org/doc/00-intro.md
40 |
--------------------------------------------------------------------------------
/Resources/doc/2-configuring_the_bundle.rst:
--------------------------------------------------------------------------------
1 | Step 2: Configuring the bundle
2 | ==============================
3 |
4 | Debug Mode
5 | ----------
6 |
7 | When debug mode is enabled a stack trace will be outputted when an exception is detected.
8 | Debug mode is automatically enabled when your app is in debug mode.
9 |
10 | You can enable or disable it manually by adding it to your configuration:
11 |
12 | .. code-block:: yaml
13 |
14 | # app/config/config.yml
15 | mediamonks_rest_api:
16 | debug: true/false
17 |
18 | Request Matching
19 | ----------------
20 |
21 | The bundle uses regexes to check if a request should be handled by this bundle. By default the bundle matches on
22 | ``/api*` with the exception of ``/api/doc*`` so you can put your documentation there.
23 |
24 | You can override these regexes by configuring your own:
25 |
26 | .. code-block:: yaml
27 |
28 | # app/config/config.yml
29 | mediamonks_rest_api:
30 | request_matcher:
31 | whitelist: ['~^/api/$~', '~^/api~']
32 | blacklist: ['~^/api/doc~']
33 |
34 | It is also possible to simply match on a single path, in that case the whitelist and blacklist config is ignored:
35 |
36 | .. code-block:: yaml
37 |
38 | # app/config/config.yml
39 | mediamonks_rest_api:
40 | request_matcher:
41 | path: '/api'
42 |
43 | Serializer
44 | ----------
45 |
46 | You can configure the serializer which is used.
47 |
48 | By default a json serializer is configured.
49 |
50 | .. code-block:: yaml
51 |
52 | # app/config/config.yml
53 | mediamonks_rest_api:
54 | serializer: json
55 |
56 | Post Message Origin
57 | -------------------
58 |
59 | Because of security reasons the default post message origin is empty by default.
60 |
61 | You can set it by adding it to your configuration:
62 |
63 | .. code-block:: yaml
64 |
65 | # app/config/config.yml
66 | mediamonks_rest_api:
67 | post_message_origin: http://www.mediamonks.com/
68 |
69 | Response Model
70 | --------------
71 |
72 | Since this bundle was originally created according to the internal api spec of MediaMonks this is the default behavior.
73 | However it is possible to override this by creating your own class which implements the
74 | ``MediaMonks\RestApi\Model\ResponseModelInterface``. You can then use the ``response_model`` option to point to the
75 | service id of your own response model.
76 |
77 | .. code-block:: yaml
78 |
79 | # app/config/config.yml
80 | mediamonks_rest_api:
81 | response_model: service_id_of_your_response_model_class
82 |
--------------------------------------------------------------------------------
/Resources/doc/3-using_the_bundle.rst:
--------------------------------------------------------------------------------
1 | Step 3: Using the bundle
2 | ========================
3 |
4 | Basic Usage
5 | -----------
6 |
7 | Using this bundle is very easy, you can simply return scalars, arrays or objects from your controller and the bundle
8 | will serialize and output the content according to the specification.
9 |
10 | .. code-block:: php
11 |
12 | 'My Value']);
62 | }
63 | }
64 |
65 | .. note::
66 |
67 | If you want to return a non-scalar response instead but still want to have control over your headers you can return
68 | an instance of MediaMonks\RestApi\Response\Response instead.
69 |
70 | Pagination
71 | ----------
72 |
73 | .. code-block:: php
74 |
75 | createFormBuilder()->getForm();
136 | $form->handleRequest($request);
137 | if (!$form->isValid()) {
138 | throw new FormValidationException($form);
139 | }
140 | // other code for handling your form
141 | }
142 |
143 | public function customValidationExceptionAction(Request $request)
144 | {
145 | throw new ValidationException([
146 | new ErrorField('field', 'code', 'message')
147 | ]);
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/Resources/doc/configuration_reference.rst:
--------------------------------------------------------------------------------
1 | MediaMonksRestApiBundle Configuration Reference
2 | ===============================================
3 |
4 | All available configuration options are listed below with their default values.
5 |
6 | .. code-block:: yaml
7 |
8 | mediamonks_rest_api:
9 | debug: %kernel.debug%
10 | post_message_origin:
11 | request_matcher:
12 | path: /api
13 | whitelist: [~^/api/$~, ~^/api~]
14 | blacklist: [~^/api/doc~]
15 | serializer: json
16 | response_model: mediamonks_rest_api.response_model
17 |
--------------------------------------------------------------------------------
/Tests/DependencyInjection/MediaMonksRestApiExtensionTest.php:
--------------------------------------------------------------------------------
1 | load();
23 | $this->assertContainerBuilderHasParameter('mediamonks_rest_api.rest_api_event_subscriber.class');
24 | $this->assertContainerBuilderHasParameter('mediamonks_rest_api.regex_request_matcher.class');
25 | $this->assertContainerBuilderHasParameter('mediamonks_rest_api.path_request_matcher.class');
26 | $this->assertContainerBuilderHasParameter('mediamonks_rest_api.request_transformer.class');
27 | $this->assertContainerBuilderHasParameter('mediamonks_rest_api.response_transformer.class');
28 | $this->assertContainerBuilderHasParameter('mediamonks_rest_api.serializer.jms.class');
29 | $this->assertContainerBuilderHasParameter('mediamonks_rest_api.serializer.json.class');
30 | $this->assertContainerBuilderHasParameter('mediamonks_rest_api.serializer.msgpack.class');
31 | $this->assertContainerBuilderHasParameter('mediamonks_rest_api.response_model.class');
32 | $this->assertContainerBuilderHasParameter('mediamonks_rest_api.response_model_factory.class');
33 | }
34 |
35 | public function testAfterLoadingTheCorrectServicesAreLoaded()
36 | {
37 | $this->load();
38 | $this->assertContainerBuilderHasService('mediamonks_rest_api.rest_api_event_subscriber');
39 | $this->assertContainerBuilderHasService('mediamonks_rest_api.regex_request_matcher');
40 | $this->assertContainerBuilderHasService('mediamonks_rest_api.path_request_matcher');
41 | $this->assertContainerBuilderHasService('mediamonks_rest_api.request_transformer');
42 | $this->assertContainerBuilderHasService('mediamonks_rest_api.response_transformer');
43 | $this->assertContainerBuilderHasService('mediamonks_rest_api.serializer.json');
44 | $this->assertContainerBuilderHasService('mediamonks_rest_api.serializer.msgpack');
45 | $this->assertContainerBuilderHasService('mediamonks_rest_api.serializer.jms');
46 | $this->assertContainerBuilderHasService('mediamonks_rest_api.response_model');
47 | $this->assertContainerBuilderHasService('mediamonks_rest_api.response_model_factory');
48 | }
49 |
50 | public function testGetDebugFromConfig()
51 | {
52 | $containerBuilder = m::mock(ContainerBuilder::class);
53 |
54 | $container = new MediaMonksRestApiExtension();
55 | $this->assertTrue($container->getDebug(['debug' => true], $containerBuilder));
56 | }
57 |
58 | public function testGetDebugFromKernel()
59 | {
60 | $containerBuilder = m::mock(ContainerBuilder::class);
61 | $containerBuilder->shouldReceive('hasParameter')->withArgs(['kernel.debug'])->andReturn(true);
62 | $containerBuilder->shouldReceive('getParameter')->withArgs(['kernel.debug'])->andReturn(true);
63 |
64 | $container = new MediaMonksRestApiExtension();
65 | $this->assertTrue($container->getDebug([], $containerBuilder));
66 | }
67 |
68 | public function testUsePathFromConfig()
69 | {
70 | $this->load(['request_matcher' => ['path' => '/foo']]);
71 |
72 | $this->assertEquals(
73 | 'mediamonks_rest_api.path_request_matcher',
74 | (string)$this->container->getDefinition('mediamonks_rest_api.rest_api_event_subscriber')->getArgument(0)
75 | );
76 | }
77 |
78 | public function testUseWhitelistFromConfig()
79 | {
80 | $this->load(['request_matcher' => ['whitelist' => ['~/foo~']]]);
81 |
82 | $this->assertEquals(
83 | 'mediamonks_rest_api.regex_request_matcher',
84 | (string)$this->container->getDefinition('mediamonks_rest_api.rest_api_event_subscriber')->getArgument(0)
85 | );
86 | }
87 |
88 | public function testUseCustomResponseModel()
89 | {
90 | $this->load(['response_model' => 'custom_response_model']);
91 |
92 | $this->assertEquals(
93 | 'custom_response_model',
94 | (string)$this->container->getDefinition('mediamonks_rest_api.response_model_factory')->getArgument(0)
95 | );
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Tests/Functional/ApiControllerTest.php:
--------------------------------------------------------------------------------
1 | requestGet('empty', Response::HTTP_NO_CONTENT);
13 | $this->assertEquals(null, $response);
14 | }
15 |
16 | public function testStringResponse()
17 | {
18 | $response = $this->requestGet('string');
19 | $this->assertArrayHasKey('data', $response);
20 | $this->assertIsScalar( $response['data']);
21 | $this->assertEquals('foobar', $response['data']);
22 | }
23 |
24 | public function testIntegerResponse()
25 | {
26 | $response = $this->requestGet('integer');
27 | $this->assertArrayHasKey('data', $response);
28 | $this->assertIsInt($response['data']);
29 | $this->assertEquals(42, $response['data']);
30 | }
31 |
32 | public function testArrayResponse()
33 | {
34 | $response = $this->requestGet('array');
35 | $this->assertArrayHasKey('data', $response);
36 | $this->assertIsArray($response['data']);
37 | $this->assertEquals(['foo', 'bar'], $response['data']);
38 | }
39 |
40 | public function testObjectResponse()
41 | {
42 | $response = $this->requestGet('object');
43 | $this->assertArrayHasKey('data', $response);
44 | $this->assertIsArray($response['data']);
45 | }
46 |
47 | public function testSymfonyResponse()
48 | {
49 | $response = $this->requestGet('symfony', Response::HTTP_CREATED);
50 | $this->assertArrayHasKey('data', $response);
51 | $this->assertIsScalar($response['data']);
52 | $this->assertEquals('foobar', $response['data']);
53 | }
54 |
55 | public function testOffsetPaginated()
56 | {
57 | $response = $this->requestGet('paginated/offset');
58 | $this->assertIsScalar($response['data']);
59 | $this->assertArrayHasKey('data', $response);
60 | $this->assertEquals('foobar', $response['data']);
61 | $this->assertArrayHasKey('pagination', $response);
62 | $this->assertArrayHasKey('offset', $response['pagination']);
63 | $this->assertEquals(1, $response['pagination']['offset']);
64 | $this->assertArrayHasKey('limit', $response['pagination']);
65 | $this->assertEquals(2, $response['pagination']['limit']);
66 | $this->assertArrayHasKey('total', $response['pagination']);
67 | $this->assertEquals(3, $response['pagination']['total']);
68 | }
69 |
70 | public function testCursorPaginated()
71 | {
72 | $response = $this->requestGet('paginated/cursor');
73 | $this->assertIsScalar($response['data']);
74 | $this->assertArrayHasKey('data', $response);
75 | $this->assertEquals('foobar', $response['data']);
76 | $this->assertArrayHasKey('pagination', $response);
77 | $this->assertArrayHasKey('before', $response['pagination']);
78 | $this->assertEquals(1, $response['pagination']['before']);
79 | $this->assertArrayHasKey('after', $response['pagination']);
80 | $this->assertEquals(2, $response['pagination']['after']);
81 | $this->assertArrayHasKey('limit', $response['pagination']);
82 | $this->assertEquals(3, $response['pagination']['limit']);
83 | $this->assertArrayHasKey('total', $response['pagination']);
84 | $this->assertEquals(4, $response['pagination']['total']);
85 | }
86 |
87 | public function testSymfonyRedirect()
88 | {
89 | $response = $this->requestGet('redirect', Response::HTTP_SEE_OTHER);
90 | $this->assertArrayHasKey('location', $response);
91 | $this->assertEquals('http://www.mediamonks.com', $response['location']);
92 | }
93 |
94 | public function testException()
95 | {
96 | $response = $this->requestGet('exception', Response::HTTP_INTERNAL_SERVER_ERROR);
97 | $this->assertErrorResponse($response);
98 | }
99 |
100 | public function testExceptionInvalidHttpStatusCode()
101 | {
102 | $response = $this->requestGet('exception-invalid-http-status-code', Response::HTTP_INTERNAL_SERVER_ERROR);
103 | $this->assertErrorResponse($response);
104 | }
105 |
106 | public function testExceptionValidCode()
107 | {
108 | $response = $this->requestGet('exception-valid-http-status-code', Response::HTTP_BAD_REQUEST);
109 | $this->assertErrorResponse($response);
110 | }
111 |
112 | public function testSymfonyNotFoundException()
113 | {
114 | $response = $this->requestGet('exception-not-found', Response::HTTP_NOT_FOUND);
115 | $this->assertErrorResponse($response);
116 | }
117 |
118 | public function testEmptyFormValidationException()
119 | {
120 | $response = $this->requestPost('empty-form', [], Response::HTTP_BAD_REQUEST);
121 |
122 | $this->assertErrorResponse($response, true, [
123 | [
124 | 'field' => '#',
125 | 'code' => 'error.form.validation.general',
126 | 'message' => 'Some general error at root level.'
127 | ]
128 | ]);
129 | }
130 |
131 | public function testFormValidationException()
132 | {
133 | $response = $this->requestPost('form', ['email' => 'foo'], Response::HTTP_BAD_REQUEST);
134 | $this->assertErrorResponse($response, true, [
135 | [
136 | 'field' => 'name',
137 | 'code' => 'error.form.validation.not_blank',
138 | 'message' => 'This value should not be blank.'
139 | ],
140 | [
141 | 'field' => 'email',
142 | 'code' => 'error.form.validation.email',
143 | 'message' => 'This value is not a valid email address.'
144 | ],
145 | [
146 | 'field' => 'email',
147 | 'code' => 'error.form.validation.length',
148 | 'message' => 'This value is too short. It should have 5 characters or more.'
149 | ]
150 | ]);
151 | }
152 |
153 | public function testFormValidationSuccess()
154 | {
155 | $response = $this->requestPost('form', ['name' => 'Robert', 'email' => 'robert@mediamonks.com'],
156 | Response::HTTP_CREATED);
157 | $this->assertEquals('foobar', $response['data']);
158 | }
159 |
160 | public function testValidationException()
161 | {
162 | $response = $this->requestGet('exception-validation', Response::HTTP_BAD_REQUEST);
163 | $this->assertErrorResponse($response, true);
164 | }
165 |
166 | public function testMethodNotAllowedException()
167 | {
168 | $response = $this->requestGet('form', Response::HTTP_METHOD_NOT_ALLOWED);
169 | $this->assertErrorResponse($response);
170 | $this->assertEquals('error.http.method_not_allowed', $response['error']['code']);
171 | $this->assertEquals('No route found for "GET /api/form": Method Not Allowed (Allow: POST)',
172 | $response['error']['message']);
173 | }
174 |
175 | public function testNotFoundHttpException()
176 | {
177 | $response = $this->requestGet('non-existing-path', Response::HTTP_NOT_FOUND);
178 | $this->assertErrorResponse($response);
179 | $this->assertEquals('error.http.not_found', $response['error']['code']);
180 | $this->assertEquals('No route found for "GET /api/non-existing-path"', $response['error']['message']);
181 | }
182 |
183 | /**
184 | * @dataProvider forceStatus200Provider
185 | *
186 | * @param string $path
187 | * @param int $statusCode
188 | */
189 | public function testForceStatusCode200(string $path, int $statusCode)
190 | {
191 | $headers = [
192 | 'HTTP_X-Force-Status-Code-200' => 1
193 | ];
194 |
195 | $response = $this->requestGet($path, Response::HTTP_OK, $headers);
196 | $this->assertEquals($response['statusCode'], $statusCode);
197 | }
198 |
199 | public function forceStatus200Provider()
200 | {
201 | yield ['empty', Response::HTTP_NO_CONTENT];
202 | yield ['string', Response::HTTP_OK];
203 | yield ['symfony', Response::HTTP_CREATED];
204 | }
205 |
206 | /**
207 | * @param $response
208 | * @param bool $fields
209 | * @param array $fieldData
210 | */
211 | protected function assertErrorResponse($response, $fields = false, $fieldData = [])
212 | {
213 | $this->assertArrayHasKey('error', $response);
214 | $this->assertArrayHasKey('code', $response['error']);
215 | $this->assertIsScalar( $response['error']['code']);
216 | $this->assertArrayHasKey('message', $response['error']);
217 | $this->assertIsScalar($response['error']['message']);
218 |
219 | if ($fields) {
220 | $this->assertArrayHasKey('fields', $response['error']);
221 | $this->assertIsArray($response['error']['fields']);
222 |
223 | $i = 0;
224 | foreach ($response['error']['fields'] as $field) {
225 | $this->assertArrayHasKey('field', $field);
226 | $this->assertIsScalar($field['field']);
227 | if (!empty($fieldData[$i]['field'])) {
228 | $this->assertEquals($fieldData[$i]['field'], $field['field']);
229 | }
230 | $this->assertArrayHasKey('code', $field);
231 | $this->assertIsScalar($field['code']);
232 | if (!empty($fieldData[$i]['code'])) {
233 | $this->assertEquals($fieldData[$i]['code'], $field['code']);
234 | }
235 | $this->assertArrayHasKey('message', $field);
236 | $this->assertIsScalar( $field['message']);
237 | if (!empty($fieldData[$i]['message'])) {
238 | $this->assertEquals($fieldData[$i]['message'], $field['message']);
239 | }
240 |
241 | $i++;
242 | }
243 | }
244 | }
245 |
246 | /**
247 | * @param $path
248 | * @param int $httpCode
249 | * @param array $headers
250 | * @return mixed
251 | */
252 | protected function requestGet($path, $httpCode = Response::HTTP_OK, $headers = [])
253 | {
254 | return $this->request('GET', $path, [], $httpCode, $headers);
255 | }
256 |
257 | /**
258 | * @param $path
259 | * @param array $data
260 | * @param int $httpCode
261 | * @param array $headers
262 | * @return mixed
263 | */
264 | protected function requestPost($path, $data = [], $httpCode = Response::HTTP_CREATED, $headers = [])
265 | {
266 | return $this->request('POST', $path, $data, $httpCode, $headers);
267 | }
268 |
269 | /**
270 | * @param string $method
271 | * @param string $path
272 | * @param array $data
273 | * @param int $httpCode
274 | * @param array $headers
275 | * @return mixed
276 | */
277 | protected function request($method, $path, array $data = [], $httpCode = Response::HTTP_OK, $headers = [])
278 | {
279 | $client = static::createClient();
280 | $client->request($method, sprintf('/api/%s', $path), $data, [], $headers);
281 | $this->assertEquals($httpCode, $client->getResponse()->getStatusCode());
282 | return json_decode($client->getResponse()->getContent(), true);
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/Tests/Functional/config/bundles.php:
--------------------------------------------------------------------------------
1 | ['all' => true],
5 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
6 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
7 | MediaMonks\RestApiBundle\MediaMonksRestApiBundle::class => ['all' => true],
8 | ];
9 |
--------------------------------------------------------------------------------
/Tests/Functional/config/packages/framework.yaml:
--------------------------------------------------------------------------------
1 | parameters:
2 | kernel.secret: "MediaMonksRestApiBundleSecret"
3 |
4 | framework:
5 | test: ~
6 | secret: "%kernel.secret%"
7 | form: ~
8 | default_locale: "en"
9 | session:
10 | handler_id: ~
11 | fragments: ~
12 | http_method_override: true
13 |
--------------------------------------------------------------------------------
/Tests/Functional/config/packages/twig.yaml:
--------------------------------------------------------------------------------
1 | twig:
2 | debug: "%kernel.debug%"
3 | strict_variables: "%kernel.debug%"
4 |
--------------------------------------------------------------------------------
/Tests/Functional/config/packages/validator.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | validation:
3 | email_validation_mode: html5
4 |
--------------------------------------------------------------------------------
/Tests/Functional/config/routes.yaml:
--------------------------------------------------------------------------------
1 | #index:
2 | # path: /
3 | # controller: App\Controller\DefaultController::index
4 |
--------------------------------------------------------------------------------
/Tests/Functional/config/routes/annotations.yaml:
--------------------------------------------------------------------------------
1 | controllers:
2 | resource: ../../src/Controller/
3 | type: annotation
4 |
--------------------------------------------------------------------------------
/Tests/Functional/config/services.yaml:
--------------------------------------------------------------------------------
1 | parameters:
2 |
3 | services:
4 | _defaults:
5 | autowire: true
6 | autoconfigure: true
7 |
8 | App\:
9 | resource: '../src/*'
10 | exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
11 |
12 | App\Controller\:
13 | resource: '../src/Controller'
14 | tags: ['controller.service_arguments']
15 |
--------------------------------------------------------------------------------
/Tests/Functional/src/Controller/ApiController.php:
--------------------------------------------------------------------------------
1 | foo = 'bar';
63 |
64 | return $object;
65 | }
66 |
67 | /**
68 | * @Route("symfony")
69 | */
70 | public function symfonyResponseAction()
71 | {
72 | return new Response('foobar', Response::HTTP_CREATED);
73 | }
74 |
75 | /**
76 | * @Route("paginated/offset")
77 | */
78 | public function offsetPaginatedAction()
79 | {
80 | return new OffsetPaginatedResponse('foobar', 1, 2, 3);
81 | }
82 |
83 | /**
84 | * @Route("paginated/cursor")
85 | */
86 | public function cursorPaginatedAction()
87 | {
88 | return new CursorPaginatedResponse('foobar', 1, 2, 3, 4);
89 | }
90 |
91 | /**
92 | * @Route("redirect")
93 | */
94 | public function symfonyRedirectAction()
95 | {
96 | return $this->redirect(
97 | 'http://www.mediamonks.com',
98 | Response::HTTP_SEE_OTHER
99 | );
100 | }
101 |
102 | /**
103 | * @Route("exception")
104 | */
105 | public function exceptionAction()
106 | {
107 | throw new \Exception('Foo'); // will return 500 Internal Server Error
108 | }
109 |
110 | /**
111 | * @Route("exception-invalid-http-status-code")
112 | */
113 | public function exceptionInvalidHttpStatusCodeAction()
114 | {
115 | throw new \Exception(
116 | 'foo', 900
117 | ); // will return 500 Internal Server Error
118 | }
119 |
120 | /**
121 | * @Route("exception-valid-http-status-code")
122 | */
123 | public function exceptionValidCodeAction()
124 | {
125 | throw new \Exception(
126 | 'foo', Response::HTTP_BAD_REQUEST
127 | ); // will return 400 Bad Request
128 | }
129 |
130 | /**
131 | * @Route("exception-not-found")
132 | */
133 | public function symfonyNotFoundExceptionAction()
134 | {
135 | throw new NotFoundHttpException('foo'); // will return 404 Not Found
136 | }
137 |
138 | /**
139 | * @Route("empty-form", methods={"POST"})
140 | */
141 | public function emptyFormValidationExceptionAction()
142 | {
143 | $form = $this->createFormBuilder()->getForm();
144 | $form->submit([]);
145 | $form->addError(new FormError('Some general error at root level.'));
146 | throw new FormValidationException($form);
147 | }
148 |
149 | /**
150 | * @param Request $request
151 | *
152 | * @return Response
153 | * @throws FormValidationException
154 | *
155 | * @Route("form", methods={"POST"})
156 | */
157 | public function formValidationExceptionAction(Request $request)
158 | {
159 | $form = $this->createForm(TestType::class);
160 | $form->submit($request->request->all());
161 |
162 | if (!$form->isValid()) {
163 | throw new FormValidationException($form);
164 | }
165 |
166 | return new Response('foobar', Response::HTTP_CREATED);
167 | }
168 |
169 | /**
170 | * @Route("exception-validation")
171 | */
172 | public function validationExceptionAction()
173 | {
174 | throw new ValidationException(
175 | [
176 | new ErrorField('field', 'code', 'message'),
177 | ]
178 | );
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/Tests/Functional/src/Form/Type/TestType.php:
--------------------------------------------------------------------------------
1 | add(
18 | 'name',
19 | TextType::class,
20 | [
21 | 'constraints' => [
22 | new NotBlank,
23 | new Length(['min' => 3, 'max' => 255])
24 | ]
25 | ]
26 | )
27 | ->add(
28 | 'email',
29 | TextType::class,
30 | [
31 | 'constraints' => [
32 | new NotBlank,
33 | new Email,
34 | new Length(['min' => 5, 'max' => 255])
35 | ]
36 | ]
37 | )
38 | ;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/Functional/src/Kernel.php:
--------------------------------------------------------------------------------
1 | getProjectDir().'/config/bundles.php';
21 | foreach ($contents as $class => $envs) {
22 | if ($envs[$this->environment] ?? $envs['all'] ?? false) {
23 | yield new $class();
24 | }
25 | }
26 | }
27 |
28 | public function getProjectDir(): string
29 | {
30 | return dirname(__DIR__);
31 | }
32 |
33 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
34 | {
35 | $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php'));
36 | $container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || !ini_get('opcache.preload'));
37 | $container->setParameter('container.dumper.inline_factories', true);
38 | $confDir = $this->getProjectDir().'/config';
39 |
40 | $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob');
41 | $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob');
42 | $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob');
43 | $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob');
44 | }
45 |
46 | protected function configureRoutes(RouteCollectionBuilder $routes): void
47 | {
48 | $confDir = $this->getProjectDir().'/config';
49 |
50 | $routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob');
51 | $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob');
52 | $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob');
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Tests/Functional/var/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 | !doctrine
4 | !cache
5 | !logs
--------------------------------------------------------------------------------
/Tests/MediaMonksRestApiBundleTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(
15 | MediaMonksRestApiExtension::class,
16 | $bundle->getContainerExtension()
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | ./Tests
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ./
22 |
23 | ./Resources
24 | ./Tests
25 | ./vendor
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------