├── .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 | [![Build Status](https://travis-ci.org/mediamonks/symfony-rest-api-bundle.svg?branch=master)](https://travis-ci.org/mediamonks/symfony-rest-api-bundle) 2 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/mediamonks/symfony-rest-api-bundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/mediamonks/symfony-rest-api-bundle/?branch=master) 3 | [![Code Coverage](https://scrutinizer-ci.com/g/mediamonks/symfony-rest-api-bundle/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/mediamonks/symfony-rest-api-bundle/?branch=master) 4 | [![Total Downloads](https://poser.pugx.org/mediamonks/rest-api-bundle/downloads)](https://packagist.org/packages/mediamonks/rest-api-bundle) 5 | [![Latest Stable Version](https://poser.pugx.org/mediamonks/rest-api-bundle/v/stable)](https://packagist.org/packages/mediamonks/rest-api-bundle) 6 | [![Latest Unstable Version](https://poser.pugx.org/mediamonks/rest-api-bundle/v/unstable)](https://packagist.org/packages/mediamonks/rest-api-bundle) 7 | [![SensioLabs Insight](https://img.shields.io/sensiolabs/i/c42e43fd-9c7b-47e1-8264-3a98961e9236.svg)](https://insight.sensiolabs.com/projects/c42e43fd-9c7b-47e1-8264-3a98961e9236) 8 | [![License](https://poser.pugx.org/mediamonks/rest-api-bundle/license)](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 | --------------------------------------------------------------------------------