├── .github └── workflows │ └── continuous-integration.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── docs ├── 0-requirements.rst ├── 1-setup_custom.rst ├── 1-setup_drupal_8.rst ├── 1-setup_silex.rst ├── 1-setup_symfony_framework.rst ├── 2-configuration.rst └── 3-usage.rst ├── phpunit.xml ├── src ├── EventSubscriber │ ├── RestApiEventSubscriber.php │ └── RestApiEventSubscriberFactory.php ├── Exception │ ├── AbstractException.php │ ├── AbstractValidationException.php │ ├── ErrorField.php │ ├── ExceptionInterface.php │ ├── FieldExceptionInterface.php │ ├── FormValidationException.php │ ├── SerializerException.php │ └── ValidationException.php ├── Model │ ├── AbstractResponseModel.php │ ├── ResponseModel.php │ ├── ResponseModelFactory.php │ └── ResponseModelInterface.php ├── Request │ ├── AbstractRequestMatcher.php │ ├── Format.php │ ├── PathRequestMatcher.php │ ├── RegexRequestMatcher.php │ ├── RequestMatcherInterface.php │ ├── RequestTransformer.php │ └── RequestTransformerInterface.php ├── Response │ ├── AbstractPaginatedResponse.php │ ├── CursorPaginatedResponse.php │ ├── Error.php │ ├── ExtendedResponseInterface.php │ ├── JsonResponse.php │ ├── OffsetPaginatedResponse.php │ ├── PaginatedResponseInterface.php │ ├── Response.php │ ├── ResponseTransformer.php │ └── ResponseTransformerInterface.php ├── Serializer │ ├── ChainSerializer.php │ ├── JMSSerializer.php │ ├── JsonSerializer.php │ ├── MsgpackSerializer.php │ ├── SerializerInterface.php │ └── SerializerTrait.php └── Util │ └── StringUtil.php └── tests ├── EventSubscriber ├── RestApiEventSubscriberFactoryTest.php └── RestApiEventSubscriberTest.php ├── Exception ├── ErrorFieldTest.php ├── FormValidationExceptionTest.php ├── JsonSerializableException.php └── ValidationExceptionTest.php ├── Form └── Type │ └── TestType.php ├── Model ├── ResponseModelFactoryTest.php └── ResponseModelTest.php ├── Request ├── FormatTest.php ├── PathRequestMatcherTest.php ├── RegexRequestMatcherTest.php └── RequestTransformerTest.php ├── Response ├── CursorPaginatedResponseTest.php ├── JsonResponseTest.php ├── OffsetPaginatedResponseTest.php ├── ResponseTest.php └── ResponseTransformerTest.php ├── Serializer ├── ChainSerializerTest.php ├── JMSSerializerTest.php ├── JsonSerializerTest.php └── MsgpackSerializerTest.php ├── Util └── StringUtilTest.php └── bootstrap.php /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | env: 10 | COMPOSER_ROOT_VERSION: "1.99.99" 11 | 12 | jobs: 13 | coverage: 14 | name: "Coverage" 15 | runs-on: "ubuntu-latest" 16 | steps: 17 | - uses: "actions/checkout@v2" 18 | - uses: "shivammathur/setup-php@v2" 19 | with: 20 | php-version: "latest" 21 | coverage: "pcov" 22 | ini-values: "memory_limit=-1, zend.assertions=1, error_reporting=-1, display_errors=On" 23 | tools: "composer" 24 | - name: "Prepare for tests" 25 | run: "mkdir -p build/logs" 26 | - uses: "ramsey/composer-install@v2" 27 | - name: "Run unit tests" 28 | run: "./vendor/bin/phpunit --colors=always --coverage-clover build/logs/clover.xml --coverage-text" 29 | - name: "Publish coverage report to Codecov" 30 | uses: "codecov/codecov-action@v2" 31 | 32 | unit-tests: 33 | name: "Unit Tests" 34 | runs-on: "ubuntu-latest" 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | php-version: [ "8.0", "8.1" ] 39 | steps: 40 | - uses: "actions/checkout@v2" 41 | - uses: "shivammathur/setup-php@v2" 42 | with: 43 | php-version: "${{ matrix.php-version }}" 44 | coverage: "none" 45 | ini-values: "memory_limit=-1, zend.assertions=1, error_reporting=-1, display_errors=On" 46 | tools: "composer" 47 | - name: "Prepare for tests" 48 | run: "mkdir -p build/logs" 49 | - uses: "ramsey/composer-install@v2" 50 | - name: "Run unit tests" 51 | run: "./vendor/bin/phpunit --colors=always" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.0-cli 2 | 3 | RUN apt-get update && apt-get install -y git unzip && pecl install pcov && pecl install msgpack && docker-php-ext-enable pcov && docker-php-ext-enable msgpack && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 4 | RUN curl -sS https://getcomposer.org/installer | php \ 5 | && mv composer.phar /usr/local/bin/composer 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docker-build: 2 | docker build -t php8-cli . 3 | 4 | deps: 5 | docker run -it --tty --rm --volume $(PWD):/app -w /app php8-cli bash -c "composer install" 6 | 7 | test: 8 | docker run -it --tty --rm --volume $(PWD):/app -w /app php8-cli bash -c "vendor/bin/phpunit -c phpunit.xml --coverage-html .coverage" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://img.shields.io/github/workflow/status/mediamonks/php-rest-api/CI?label=CI&logo=github&style=flat-square)](https://github.com/mediamonks/php-rest-api/actions?query=workflow%3ACI) 2 | [![Code Coverage](https://img.shields.io/codecov/c/gh/mediamonks/php-rest-api?label=codecov&logo=codecov&style=flat-square)](https://codecov.io/gh/mediamonks/php-rest-api) 3 | [![Total Downloads](https://poser.pugx.org/mediamonks/rest-api/downloads)](https://packagist.org/packages/mediamonks/rest-api) 4 | [![Latest Stable Version](https://poser.pugx.org/mediamonks/rest-api/v/stable)](https://packagist.org/packages/mediamonks/rest-api) 5 | [![Latest Unstable Version](https://poser.pugx.org/mediamonks/rest-api/v/unstable)](https://packagist.org/packages/mediamonks/rest-api) 6 | [![License](https://poser.pugx.org/mediamonks/rest-api/license)](https://packagist.org/packages/mediamonks/rest-api) 7 | 8 | # MediaMonks Rest API 9 | 10 | This library contains an event subscriber to easily create a Rest API with the [Symfony HttpKernel](http://symfony.com/doc/current/components/http_kernel.html). 11 | By default this library will output according to our [MediaMonks Rest API spec](https://github.com/mediamonks/documents) but since we believe it could be very useful for other companies too it's very easy to extend it or implement your own. 12 | 13 | ## Highlights 14 | 15 | - Thrown exceptions will be converted automatically 16 | - Supports custom serializers like JMS, uses json serializer by default 17 | - Supports custom response models 18 | - Supports application/json, application/x-www-form-urlencoded & multipart/form-data input 19 | - Supports method overriding 20 | - Supports forcing a "200 OK" status method 21 | - Supports paginated responses 22 | - Supports wrapping json response in a method (jsonp) and post message 23 | - Should work with any framework that uses HttpKernel 24 | 25 | ## Documentation 26 | 27 | Documentation and examples can be found in the [/docs](/docs) folder. 28 | 29 | ## Requirements 30 | 31 | - PHP >= 8.0 32 | 33 | To use the library. 34 | 35 | ## Installation 36 | 37 | For Symfony Framework users it is recommended to install the [Rest API Bundle](https://github.com/mediamonks/symfony-rest-api-bundle) instead of this library. 38 | 39 | Install this package by using Composer. 40 | 41 | ``` 42 | $ composer require mediamonks/rest-api 43 | ``` 44 | 45 | ## Security 46 | 47 | If you discover any security related issues, please email devmonk@mediamonks.com instead of using the issue tracker. 48 | 49 | ## License 50 | 51 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediamonks/rest-api", 3 | "type": "library", 4 | "description": "MediaMonks Rest API", 5 | "keywords": [ 6 | "rest", 7 | "api", 8 | "bundle", 9 | "symfony" 10 | ], 11 | "homepage": "https://www.mediamonks.com/", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Robert Slootjes", 16 | "email": "robert@mediamonks.com", 17 | "homepage": "https://github.com/slootjes" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=8.0", 22 | "symfony/http-kernel": "^5.0|^6.0", 23 | "ext-json": "*" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^9.5", 27 | "mockery/mockery": "^1.4", 28 | "symfony/validator": "^5.0|^6.0", 29 | "symfony/form": "^5.0|^6.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "MediaMonks\\RestApi\\": "src" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "MediaMonks\\RestApi\\Tests\\": "tests" 39 | } 40 | }, 41 | "extra": { 42 | "branch-alias": { 43 | "dev-master": "2.0-dev" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/0-requirements.rst: -------------------------------------------------------------------------------- 1 | Step 0: Requirements 2 | ==================== 3 | 4 | This library needs at least PHP 8.0 to work correctly. 5 | 6 | Also, this library relies on your framework using the `Symfony HttpKernel Component`_ to handle requests and responses. 7 | 8 | .. _`Symfony HttpKernel Component`: http://symfony.com/doc/current/components/http_kernel.html 9 | -------------------------------------------------------------------------------- /docs/1-setup_custom.rst: -------------------------------------------------------------------------------- 1 | Step 1: Setting up the library 2 | ============================== 3 | 4 | A) Download the library 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 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) Setup the library 18 | -------------------- 19 | 20 | The easiest method is to create the event subscriber by using the factory method: 21 | 22 | .. code-block:: php 23 | 24 | use MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriberFactory; 25 | use Symfony\Component\EventDispatcher\EventDispatcher; 26 | 27 | $eventSubscriber = RestApiEventSubscriberFactory::create(); 28 | 29 | // register the event subscriber in the event dispatcher 30 | $dispatcher = new EventDispatcher; 31 | $dispatcher->addSubscriber($eventSubscriber); 32 | 33 | // inject the $dispatcher in your http kernel 34 | 35 | You can also do it yourself like this: 36 | 37 | .. code-block:: php 38 | 39 | $whitelist = [ 40 | '~^/api/$~', 41 | '~^/api~' 42 | ]; 43 | $blacklist = [ 44 | '~^/api/doc~' 45 | ]; 46 | $options = [ 47 | 'debug' => false, 48 | 'post_message_origin' => 'https://www.mediamonks.com' 49 | ]; 50 | 51 | // choose a serializer, we pick json as default 52 | $serializer = new MediaMonks\RestApi\Serializer\JsonSerializer(); 53 | 54 | // initialize the request matcher with the whitelist and blacklist 55 | $requestMatcher = new MediaMonks\RestApi\Request\RegexRequestMatcher($whitelist, $blacklist); 56 | 57 | // initialize the request transformer, this sets the output format as an attribute in the request 58 | $requestTransformer = new MediaMonks\RestApi\Request\RequestTransformer($serializer); 59 | 60 | // this is the model that will be used to transform your output to 61 | $responseModel = new MediaMonks\RestApi\Model\ResponseModel(); 62 | 63 | // will return a new response model for every response 64 | $responseModelFactory = new \MediaMonks\RestApi\Model\ResponseModelFactory($responseModel); 65 | 66 | // where most of the magic happens, converts any response or exception into the response model 67 | $responseTransformer = new MediaMonks\RestApi\Response\ResponseTransformer($serializer, $responseModelFactory, $options); 68 | 69 | // the subscriber that ties it all together and hooks into the HttpKernel 70 | $eventSubscriber = new MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriber( 71 | $requestMatcher, 72 | $requestTransformer, 73 | $responseTransformer 74 | ); 75 | 76 | // register the event subscriber in the event dispatcher 77 | $dispatcher = new Symfony\Component\EventDispatcher\EventDispatcher(); 78 | $dispatcher->addSubscriber($eventSubscriber); 79 | 80 | // inject the $dispatcher in your http kernel 81 | 82 | .. _`installation chapter`: https://getcomposer.org/doc/00-intro.md 83 | -------------------------------------------------------------------------------- /docs/1-setup_drupal_8.rst: -------------------------------------------------------------------------------- 1 | Step 1: Setting up the library in Drupal 8 2 | ========================================== 3 | 4 | A) Download the library 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 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) Setup the library 18 | -------------------- 19 | 20 | A custom module will be created soon but untill then this will implement the library in your Drupal 8 project: 21 | 22 | Create a module "mediamonks_rest_api" with these files: 23 | 24 | .. code-block:: yml 25 | 26 | # mediamonks_rest_api.info.yml 27 | name: MediaMonks Rest API 28 | description: Transforms any controller result into a Rest API response 29 | package: Custom 30 | type: module 31 | core: 8.x 32 | 33 | 34 | .. code-block:: yml 35 | 36 | services: 37 | MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriber: 38 | autowire: true 39 | tags: 40 | - { name: event_subscriber } 41 | 42 | MediaMonks\RestApi\Request\PathRequestMatcher: 43 | public: false 44 | arguments: 45 | - '/api' 46 | 47 | MediaMonks\RestApi\Request\RequestTransformer: 48 | public: false 49 | autowire: true 50 | 51 | MediaMonks\RestApi\Response\ResponseTransformer: 52 | public: false 53 | autowire: true 54 | 55 | MediaMonks\RestApi\Serializer\JsonSerializer: 56 | public: false 57 | 58 | MediaMonks\RestApi\Model\ResponseModel: 59 | public: false 60 | 61 | MediaMonks\RestApi\Model\ResponseModelFactory: 62 | public: false 63 | autowire: true 64 | 65 | 66 | Then activate the module, clear caches and start creating controllers which start with /api for it to take effect. 67 | 68 | Please note this example uses autowiring which is available since Drupal 8.5 69 | 70 | .. _`installation chapter`: https://getcomposer.org/doc/00-intro.md 71 | -------------------------------------------------------------------------------- /docs/1-setup_silex.rst: -------------------------------------------------------------------------------- 1 | Step 1: Setting up the library in Silex 2 | ======================================= 3 | 4 | A) Download the library 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 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) Setup the library 18 | -------------------- 19 | 20 | In the `Silex`_ micro-framework you can use this library with just a single line of code: 21 | 22 | .. code-block:: php 23 | 24 | require_once __DIR__ . '/../../vendor/autoload.php'; 25 | 26 | use MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriberFactory; 27 | use MediaMonks\RestApi\Response\JsonResponse; 28 | 29 | $app = new Silex\Application(); 30 | 31 | $app['dispatcher']->addSubscriber(RestApiEventSubscriberFactory::create()); 32 | 33 | $app->get('/api', function() { 34 | return new JsonResponse('Hello Api'); 35 | }); 36 | 37 | $app->run(); 38 | 39 | .. _`installation chapter`: https://getcomposer.org/doc/00-intro.md 40 | .. _`Silex`: http://silex.sensiolabs.org/ 41 | -------------------------------------------------------------------------------- /docs/1-setup_symfony_framework.rst: -------------------------------------------------------------------------------- 1 | Symfony Framework 2 | ================= 3 | 4 | Since `Symfony Framework`_ is our preferred framework we created a nice bundle for it: `Symfony Rest API Bundle`_. 5 | 6 | .. _`Symfony Framework`: http://symfony.com/ 7 | .. _`Symfony Rest API Bundle`: https://github.com/mediamonks/symfony-rest-api-bundle 8 | -------------------------------------------------------------------------------- /docs/2-configuration.rst: -------------------------------------------------------------------------------- 1 | Step 2: Configuration 2 | ===================== 3 | 4 | Path 5 | ---- 6 | 7 | The event subscriber is activated by the path ``/api`` by default, you can override this by passing the 'path' option: 8 | 9 | .. code-block:: php 10 | 11 | use MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriberFactory; 12 | 13 | $eventSubscriber = RestApiEventSubscriberFactory::create(['path' => '/my-api']); 14 | 15 | 16 | Debug Mode 17 | ---------- 18 | 19 | When debug mode is enabled a stack trace will be outputted when an exception is detected. 20 | Debug mode is disabled by default. 21 | 22 | .. code-block:: php 23 | 24 | use MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriberFactory; 25 | 26 | $eventSubscriber = RestApiEventSubscriberFactory::create(['debug' => true]); 27 | 28 | 29 | Request Matcher 30 | --------------- 31 | 32 | The library uses a Path matcher by default. You can also pass a different matcher if you like, as long as it imlements 33 | the ``MediaMonks\RestApi\Request\RequestMatcherInterface``: 34 | 35 | .. code-block:: php 36 | 37 | use MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriberFactory; 38 | 39 | $eventSubscriber = RestApiEventSubscriberFactory::create([ 40 | 'request_matcher' => new \My\Custom\RequestMatcher() 41 | ]); 42 | 43 | 44 | Serializer 45 | ---------- 46 | 47 | You can configure the serializer which is used. By default a json serializer is used however it is possible to override 48 | this by creating your own class which implements the ``MediaMonks\RestApi\Serializer\SerializerInterface``. 49 | 50 | You can then pass it to the create method: 51 | 52 | .. code-block:: php 53 | 54 | use MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriberFactory; 55 | 56 | $eventSubscriber = RestApiEventSubscriberFactory::create([ 57 | 'serializer' => new \My\Custom\Serializer() 58 | ]); 59 | 60 | 61 | Post Message Origin 62 | ------------------- 63 | 64 | Because of security reasons the default post message origin is empty by default. 65 | 66 | You can set it by adding it to your configuration: 67 | 68 | .. code-block:: php 69 | 70 | use MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriberFactory; 71 | 72 | $eventSubscriber = RestApiEventSubscriberFactory::create(['post_message_origin' => 'https://www.mediamonks.com']); 73 | 74 | 75 | Response Model 76 | -------------- 77 | 78 | Since this bundle was originally created according to the internal api spec of MediaMonks this is the default behavior. 79 | However it is possible to override this by creating your own class which implements the 80 | ``MediaMonks\RestApi\Model\ResponseModelInterface``. You can then pass it to the create method: 81 | 82 | .. code-block:: php 83 | 84 | use MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriberFactory; 85 | 86 | $eventSubscriber = RestApiEventSubscriberFactory::create([ 87 | 'response_model' => new \My\Custom\ResponseModel() 88 | ]); 89 | -------------------------------------------------------------------------------- /docs/3-usage.rst: -------------------------------------------------------------------------------- 1 | Step 3: Usage 2 | ============= 3 | 4 | Custom Status Code And Headers 5 | ------------------------------ 6 | 7 | It is also possible to return a regular Symfony HttpFoundation Response which allows you to set a custom http status 8 | code and headers. 9 | 10 | .. code-block:: php 11 | 12 | 'My Value']); 21 | } 22 | } 23 | 24 | .. note:: 25 | 26 | If you want to return a non-scalar response instead but still want to have control over your headers you can return 27 | an instance of MediaMonks\RestApi\Response\Response instead. 28 | 29 | Pagination 30 | ---------- 31 | 32 | .. code-block:: php 33 | 34 | createFormBuilder()->getForm(); 93 | $form->handleRequest($request); 94 | if (!$form->isValid()) { 95 | throw new FormValidationException($form); 96 | } 97 | // other code for handling your form 98 | } 99 | 100 | public function customValidationExceptionAction(Request $request) 101 | { 102 | throw new ValidationException([ 103 | new ErrorField('field', 'code', 'message') 104 | ]); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./ 16 | 17 | 18 | ./tests 19 | ./vendor 20 | 21 | 22 | 23 | 24 | ./tests/ 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/EventSubscriber/RestApiEventSubscriber.php: -------------------------------------------------------------------------------- 1 | "array[]", KernelEvents::EXCEPTION => "array[]", KernelEvents::VIEW => "array[]", KernelEvents::RESPONSE => "array[]"])] public static function getSubscribedEvents(): array 30 | { 31 | return [ 32 | KernelEvents::REQUEST => [ 33 | ['onRequest', 512], 34 | ], 35 | KernelEvents::EXCEPTION => [ 36 | ['onException', 512], 37 | ], 38 | KernelEvents::VIEW => [ 39 | ['onView', 0], 40 | ], 41 | KernelEvents::RESPONSE => [ 42 | ['onResponseEarly', 0], 43 | ['onResponseLate', -512], 44 | ], 45 | ]; 46 | } 47 | 48 | public function onRequest(RequestEvent $event): void 49 | { 50 | if (!$this->eventRequestMatches($event)) { 51 | return; 52 | } 53 | 54 | $this->requestTransformer->transform($event->getRequest()); 55 | } 56 | 57 | public function onException(ExceptionEvent $event): void 58 | { 59 | if (!$this->eventRequestMatches($event)) { 60 | return; 61 | } 62 | 63 | $event->setResponse($this->responseTransformer->createResponseFromContent($event->getThrowable())); 64 | } 65 | 66 | public function onView(ViewEvent $event): void 67 | { 68 | if (!$this->eventRequestMatches($event)) { 69 | return; 70 | } 71 | 72 | $event->setResponse($this->responseTransformer->createResponseFromContent($event->getControllerResult())); 73 | } 74 | 75 | public function onResponseEarly(ResponseEvent $event): void 76 | { 77 | if (!$this->eventRequestMatches($event)) { 78 | return; 79 | } 80 | 81 | $event->setResponse($this->responseTransformer->transformEarly($event->getRequest(), $event->getResponse())); 82 | } 83 | 84 | public function onResponseLate(ResponseEvent $event): void 85 | { 86 | if (!$this->eventRequestMatches($event)) { 87 | return; 88 | } 89 | 90 | $this->responseTransformer->transformLate($event->getRequest(), $event->getResponse()); 91 | } 92 | 93 | protected function eventRequestMatches(KernelEvent $event): bool 94 | { 95 | if ($event->getRequest()->getMethod() === Request::METHOD_OPTIONS) return false; 96 | 97 | return $this->requestMatcher->matches($event->getRequest(), $event->getRequestType()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/EventSubscriber/RestApiEventSubscriberFactory.php: -------------------------------------------------------------------------------- 1 | message, $this->code); 11 | } 12 | 13 | public function toArray(): array 14 | { 15 | $return = [ 16 | 'code' => $this->getCode(), 17 | 'message' => $this->getMessage(), 18 | ]; 19 | 20 | /** @var ExceptionInterface|array $field */ 21 | foreach ($this->getFields() as $field) { 22 | $return['fields'][] = is_array($field) ? $field : $field->toArray(); 23 | } 24 | 25 | return $return; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Exception/ErrorField.php: -------------------------------------------------------------------------------- 1 | field; 18 | } 19 | 20 | public function getMessage(): string 21 | { 22 | return $this->message; 23 | } 24 | 25 | public function getCode(): string 26 | { 27 | return $this->code; 28 | } 29 | 30 | public function toArray(): array 31 | { 32 | return [ 33 | 'field' => $this->getField(), 34 | 'code' => $this->getCode(), 35 | 'message' => $this->getMessage(), 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | getErrorMessages($this->form); 26 | } 27 | 28 | protected function getErrorMessages(FormInterface $form): array 29 | { 30 | $errors = []; 31 | foreach ($this->getFormErrorMessages($form) as $error) { 32 | $errors[] = $error; 33 | } 34 | foreach ($this->getFormChildErrorMessages($form) as $error) { 35 | $errors[] = $error; 36 | } 37 | 38 | return $errors; 39 | } 40 | 41 | protected function getFormErrorMessages(FormInterface $form): array 42 | { 43 | $errors = []; 44 | foreach ($form->getErrors() as $error) { 45 | if ($form->isRoot()) { 46 | $errors[] = $this->toErrorArray($error); 47 | } else { 48 | $errors[] = $this->toErrorArray($error, $form); 49 | } 50 | } 51 | 52 | return $errors; 53 | } 54 | 55 | protected function getFormChildErrorMessages(FormInterface $form): array 56 | { 57 | $errors = []; 58 | foreach ($form->all() as $child) { 59 | if ($this->shouldAddChildErrorMessage($child)) { 60 | foreach ($this->getErrorMessages($child) as $error) { 61 | $errors[] = $error; 62 | } 63 | } 64 | } 65 | 66 | return $errors; 67 | } 68 | 69 | protected function shouldAddChildErrorMessage(FormInterface $child = null): bool 70 | { 71 | return !empty($child) && !$child->isValid(); 72 | } 73 | 74 | protected function toErrorArray(FormError $error, FormInterface $form = null): array 75 | { 76 | if (is_null($form)) { 77 | $field = self::FIELD_ROOT; 78 | } else { 79 | $field = $form->getName(); 80 | } 81 | if (!is_null($error->getCause()) && !is_null($error->getCause()->getConstraint())) { 82 | $code = $this->getErrorCode(StringUtil::classToSnakeCase($error->getCause()->getConstraint())); 83 | } else { 84 | $code = $this->getErrorCodeByMessage($error); 85 | } 86 | 87 | return (new ErrorField($field, $code, $error->getMessage()))->toArray(); 88 | } 89 | 90 | protected function getErrorCodeByMessage(FormError $error): string 91 | { 92 | if (stristr($error->getMessage(), Error::FORM_TYPE_CSRF)) { 93 | return $this->getErrorCode(Error::FORM_TYPE_CSRF); 94 | } 95 | 96 | return $this->getErrorCode(Error::FORM_TYPE_GENERAL); 97 | } 98 | 99 | protected function getErrorCode(string $value): string 100 | { 101 | return sprintf(Error::ERROR_KEY_FORM_VALIDATION.'.%s', $value); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Exception/SerializerException.php: -------------------------------------------------------------------------------- 1 | setFields($fields); 17 | parent::__construct($message, $code); 18 | } 19 | 20 | public function setFields(array $fields) 21 | { 22 | foreach ($fields as $field) { 23 | if (!$field instanceof ErrorField) { 24 | throw new \InvalidArgumentException('Every field must be an instance of ErrorField'); 25 | } 26 | $this->fields[] = $field; 27 | } 28 | } 29 | 30 | public function getFields(): array 31 | { 32 | return $this->fields; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Model/AbstractResponseModel.php: -------------------------------------------------------------------------------- 1 | returnStackTrace; 33 | } 34 | 35 | public function setReturnStackTrace(bool $returnStackTrace): ResponseModelInterface 36 | { 37 | $this->returnStackTrace = $returnStackTrace; 38 | 39 | return $this; 40 | } 41 | 42 | public function getStatusCode(): int 43 | { 44 | if (isset($this->throwable)) { 45 | return $this->getExceptionStatusCode(); 46 | } 47 | 48 | if ($this->isEmpty()) { 49 | return Response::HTTP_NO_CONTENT; 50 | } 51 | 52 | return $this->statusCode; 53 | } 54 | 55 | protected function getExceptionStatusCode(): int 56 | { 57 | if ($this->throwable instanceof HttpException) { 58 | return $this->throwable->getStatusCode(); 59 | } 60 | if ($this->throwable instanceof AbstractValidationException) { 61 | return Response::HTTP_BAD_REQUEST; 62 | } 63 | if ($this->isValidHttpStatusCode($this->throwable->getCode())) { 64 | return $this->throwable->getCode(); 65 | } 66 | 67 | return Response::HTTP_INTERNAL_SERVER_ERROR; 68 | } 69 | 70 | protected function isValidHttpStatusCode(int $code): bool 71 | { 72 | return array_key_exists($code, Response::$statusTexts) && $code >= Response::HTTP_BAD_REQUEST; 73 | } 74 | 75 | protected function getThrowableErrorCode(string $errorCode, ?string $trim = null): string 76 | { 77 | return sprintf($errorCode, StringUtil::classToSnakeCase($this->throwable, $trim)); 78 | } 79 | 80 | protected function getThrowableStackTrace(): array 81 | { 82 | $traces = []; 83 | foreach ($this->throwable->getTrace() as $trace) { 84 | // Since PHP 7.4 the args key got disabled, to enable it again: 85 | // zend.exception_ignore_args = On 86 | if (array_key_exists('args', $trace)) { 87 | $trace['args'] = json_decode(json_encode($trace['args']), true); 88 | } 89 | 90 | $traces[] = $trace; 91 | } 92 | 93 | return $traces; 94 | } 95 | 96 | public function setStatusCode(int $statusCode): ResponseModelInterface 97 | { 98 | $this->statusCode = $statusCode; 99 | 100 | return $this; 101 | } 102 | 103 | public function getReturnStatusCode(): bool 104 | { 105 | return $this->returnStatusCode; 106 | } 107 | 108 | public function setReturnStatusCode(bool $returnStatusCode): ResponseModelInterface 109 | { 110 | $this->returnStatusCode = $returnStatusCode; 111 | 112 | return $this; 113 | } 114 | 115 | public function getData(): mixed 116 | { 117 | return $this->data; 118 | } 119 | 120 | public function setData(mixed $data): ResponseModelInterface 121 | { 122 | $this->data = $data; 123 | 124 | return $this; 125 | } 126 | 127 | public function getThrowable(): ?\Throwable 128 | { 129 | return $this->throwable; 130 | } 131 | 132 | public function setThrowable(\Throwable $throwable): ResponseModelInterface 133 | { 134 | $this->throwable = $throwable; 135 | 136 | return $this; 137 | } 138 | 139 | public function getPagination(): ?PaginatedResponseInterface 140 | { 141 | return $this->pagination; 142 | } 143 | 144 | public function setPagination(PaginatedResponseInterface $pagination): ResponseModelInterface 145 | { 146 | $this->pagination = $pagination; 147 | $this->setData($pagination->getData()); 148 | 149 | return $this; 150 | } 151 | 152 | public function getResponse(): ?Response 153 | { 154 | return $this->response; 155 | } 156 | 157 | public function setResponse(Response $response): ResponseModelInterface 158 | { 159 | $this->response = $response; 160 | $this->setStatusCode($response->getStatusCode()); 161 | $this->setData($response->getContent()); 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * @return ExtendedResponseInterface 168 | */ 169 | public function getExtendedResponse() 170 | { 171 | return $this->extendedResponse; 172 | } 173 | 174 | /** 175 | * @param ExtendedResponseInterface $response 176 | * @return $this 177 | */ 178 | public function setExtendedResponse(ExtendedResponseInterface $response): ResponseModelInterface 179 | { 180 | $this->extendedResponse = $response; 181 | $this->setStatusCode($response->getStatusCode()); 182 | $this->setData($response->getCustomContent()); 183 | 184 | return $this; 185 | } 186 | 187 | public function isEmpty(): bool 188 | { 189 | return ( 190 | !isset($this->throwable) 191 | && is_null($this->data) 192 | && !isset($this->pagination) 193 | && $this->isEmptyResponse() 194 | && $this->isEmptyExtendedResponse() 195 | ); 196 | } 197 | 198 | protected function isEmptyResponse(): bool 199 | { 200 | return (!isset($this->response) || $this->response->isEmpty()); 201 | } 202 | 203 | protected function isEmptyExtendedResponse(): bool 204 | { 205 | return (!isset($this->extendedResponse) || $this->extendedResponse->isEmpty()); 206 | } 207 | 208 | // @codeCoverageIgnoreStart 209 | 210 | /** 211 | * This is called when an exception is thrown during the response transformation 212 | */ 213 | public function __toString(): string 214 | { 215 | return json_encode(get_object_vars($this)); 216 | } 217 | // @codeCoverageIgnoreEnd 218 | } 219 | -------------------------------------------------------------------------------- /src/Model/ResponseModel.php: -------------------------------------------------------------------------------- 1 | getReturnStatusCode()) { 22 | $return['statusCode'] = $this->getStatusCode(); 23 | } 24 | 25 | if (isset($this->throwable)) { 26 | $return['error'] = $this->throwableToArray(); 27 | } elseif (isset($this->response) && $this->response instanceof RedirectResponse) { 28 | $return['location'] = $this->response->headers->get('Location'); 29 | } else { 30 | $return += $this->dataToArray(); 31 | } 32 | 33 | return $return; 34 | } 35 | 36 | protected function dataToArray(): array 37 | { 38 | $return = []; 39 | if (isset($this->data)) { 40 | $return['data'] = $this->data; 41 | if (isset($this->pagination)) { 42 | $return['pagination'] = $this->pagination->toArray(); 43 | } 44 | } 45 | 46 | return $return; 47 | } 48 | 49 | protected function throwableToArray(): array 50 | { 51 | if ($this->throwable instanceof ExceptionInterface) { 52 | $error = $this->throwable->toArray(); 53 | } elseif ($this->throwable instanceof HttpException) { 54 | $error = $this->httpExceptionToArray(); 55 | } elseif ($this->throwable instanceof JsonSerializable) { 56 | $error = $this->throwable->jsonSerialize(); 57 | } else { 58 | $error = $this->generalThrowableToArray(); 59 | } 60 | 61 | if ($this->isReturnStackTrace()) { 62 | $error['stack_trace'] = $this->getThrowableStackTrace(); 63 | } 64 | 65 | return $error; 66 | } 67 | 68 | protected function httpExceptionToArray(): array 69 | { 70 | return [ 71 | 'code' => $this->getThrowableErrorCode( 72 | Error::ERROR_KEY_HTTP, 73 | self::EXCEPTION_HTTP 74 | ), 75 | 'message' => $this->throwable->getMessage(), 76 | ]; 77 | } 78 | 79 | protected function generalThrowableToArray(): array 80 | { 81 | return [ 82 | 'code' => trim( 83 | $this->getThrowableErrorCode( 84 | Error::ERROR_KEY_GENERAL, 85 | self::EXCEPTION_GENERAL 86 | ), 87 | '.' 88 | ), 89 | 'message' => $this->throwable->getMessage(), 90 | ]; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Model/ResponseModelFactory.php: -------------------------------------------------------------------------------- 1 | createFromExtendedResponse($content); 20 | } 21 | if ($content instanceof Response) { 22 | return $this->createFromResponse($content); 23 | } 24 | if ($content instanceof PaginatedResponseInterface) { 25 | return $this->createFromPaginatedResponse($content); 26 | } 27 | if ($content instanceof \Throwable) { 28 | return $this->createFromThrowable($content); 29 | } 30 | 31 | return $this->create()->setData($content); 32 | } 33 | 34 | public function createFromExtendedResponse(ExtendedResponseInterface $response): ResponseModelInterface 35 | { 36 | return $this->create()->setExtendedResponse($response); 37 | } 38 | 39 | public function createFromResponse(Response $response): ResponseModelInterface 40 | { 41 | return $this->create()->setResponse($response); 42 | } 43 | 44 | public function createFromPaginatedResponse(PaginatedResponseInterface $response): ResponseModelInterface 45 | { 46 | return $this->create()->setPagination($response); 47 | } 48 | 49 | public function createFromThrowable(\Throwable $throwable): ResponseModelInterface 50 | { 51 | return $this->create()->setThrowable($throwable); 52 | } 53 | 54 | private function create(): ResponseModelInterface 55 | { 56 | return clone $this->responseModel; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Model/ResponseModelInterface.php: -------------------------------------------------------------------------------- 1 | matchPreviouslyMatchedRequest($request)) { 22 | return true; 23 | } 24 | 25 | if (!$this->matchRequestPathAgainstLists($request->getPathInfo())) { 26 | return false; 27 | } 28 | 29 | $this->markRequestAsMatched($request); 30 | 31 | return true; 32 | } 33 | 34 | protected function markRequestAsMatched(Request $request) 35 | { 36 | $request->attributes->set(self::ATTRIBUTE_MATCHED, true); 37 | } 38 | 39 | protected function matchPreviouslyMatchedRequest(Request $request): bool 40 | { 41 | return $request->attributes->getBoolean(self::ATTRIBUTE_MATCHED); 42 | } 43 | 44 | protected function matchRequestPathAgainstLists($requestPath): bool 45 | { 46 | if ($this->matchRequestPathAgainstBlacklist($requestPath)) { 47 | return false; 48 | } 49 | 50 | if ($this->matchRequestPathAgainstWhitelist($requestPath)) { 51 | return true; 52 | } 53 | 54 | return false; 55 | } 56 | 57 | protected function matchRequestPathAgainstBlacklist(string $requestPath): bool 58 | { 59 | foreach ($this->blacklist as $regex) { 60 | if (preg_match($regex, $requestPath)) { 61 | return true; 62 | } 63 | } 64 | 65 | return false; 66 | } 67 | 68 | protected function matchRequestPathAgainstWhitelist(string $requestPath): bool 69 | { 70 | foreach ($this->whitelist as $regex) { 71 | if (preg_match($regex, $requestPath)) { 72 | return true; 73 | } 74 | } 75 | 76 | return false; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Request/RequestMatcherInterface.php: -------------------------------------------------------------------------------- 1 | acceptJsonBody($request); 17 | $this->setRequestFormat($request); 18 | } 19 | 20 | protected function acceptJsonBody(Request $request) 21 | { 22 | if (str_starts_with($request->headers->get('Content-Type', ''), 'application/json')) { 23 | $data = json_decode($request->getContent(), true); 24 | $request->request->replace(is_array($data) ? $data : []); 25 | } 26 | } 27 | 28 | protected function setRequestFormat(Request $request) 29 | { 30 | $default = Format::getDefault(); 31 | $format = $request->getRequestFormat($request->query->get('_format', $default)); 32 | 33 | if (!in_array($format, $this->serializer->getSupportedFormats())) { 34 | $format = $this->serializer->getDefaultFormat(); 35 | } 36 | 37 | $request->setRequestFormat($format); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Request/RequestTransformerInterface.php: -------------------------------------------------------------------------------- 1 | data; 14 | } 15 | 16 | public function setData(mixed $data): void 17 | { 18 | $this->data = $data; 19 | } 20 | 21 | public function getLimit(): int 22 | { 23 | return $this->limit; 24 | } 25 | 26 | public function setLimit(int $limit): void 27 | { 28 | $this->limit = $limit; 29 | } 30 | 31 | public function getTotal(): ?int 32 | { 33 | return $this->total; 34 | } 35 | 36 | public function setTotal(?int $total): void 37 | { 38 | $this->total = $total; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Response/CursorPaginatedResponse.php: -------------------------------------------------------------------------------- 1 | before = $before; 28 | $this->after = $after; 29 | } 30 | 31 | /** 32 | * @return mixed 33 | */ 34 | public function getBefore() 35 | { 36 | return $this->before; 37 | } 38 | 39 | /** 40 | * @param mixed $before 41 | */ 42 | public function setBefore($before) 43 | { 44 | $this->before = $before; 45 | } 46 | 47 | /** 48 | * @return mixed 49 | */ 50 | public function getAfter() 51 | { 52 | return $this->after; 53 | } 54 | 55 | /** 56 | * @param mixed $after 57 | */ 58 | public function setAfter($after) 59 | { 60 | $this->after = $after; 61 | } 62 | 63 | /** 64 | * @return array 65 | */ 66 | public function toArray() 67 | { 68 | $data = [ 69 | 'before' => $this->getBefore(), 70 | 'after' => $this->getAfter(), 71 | 'limit' => $this->getLimit(), 72 | ]; 73 | if (!is_null($this->getTotal())) { 74 | $data['total'] = $this->getTotal(); 75 | } 76 | 77 | return $data; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Response/Error.php: -------------------------------------------------------------------------------- 1 | setCustomContent($data); 28 | } 29 | 30 | /** 31 | * We need this because setData() does json encoding already and 32 | * this messes up the jsonp callback. 33 | * It is a performance hit to let it decode/encode a second time 34 | * 35 | * @param mixed $content 36 | * @return $this 37 | */ 38 | public function setCustomContent(mixed $content): static 39 | { 40 | $this->customContent = $content; 41 | 42 | return $this; 43 | } 44 | 45 | public function getCustomContent(): mixed 46 | { 47 | return $this->customContent; 48 | } 49 | 50 | public function getContent(): string|false 51 | { 52 | return is_string($this->customContent) ? $this->customContent : json_encode($this->customContent); 53 | } 54 | 55 | public function setData(mixed $data = []): static 56 | { 57 | $this->setCustomContent($data); 58 | return parent::setData($data); 59 | } 60 | 61 | /** 62 | * @return mixed 63 | */ 64 | public function getCallback() 65 | { 66 | return $this->callback; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Response/OffsetPaginatedResponse.php: -------------------------------------------------------------------------------- 1 | offset = $offset; 22 | } 23 | 24 | /** 25 | * @return int 26 | */ 27 | public function getOffset() 28 | { 29 | return $this->offset; 30 | } 31 | 32 | /** 33 | * @param int $offset 34 | */ 35 | public function setOffset($offset) 36 | { 37 | $this->offset = $offset; 38 | } 39 | 40 | /** 41 | * @return array 42 | */ 43 | public function toArray() 44 | { 45 | $data = [ 46 | 'offset' => $this->getOffset(), 47 | 'limit' => $this->getLimit(), 48 | ]; 49 | if (!is_null($this->getTotal())) { 50 | $data['total'] = $this->getTotal(); 51 | } 52 | 53 | return $data; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Response/PaginatedResponseInterface.php: -------------------------------------------------------------------------------- 1 | setCustomContent($data); 27 | } 28 | 29 | /** 30 | * Sets the response content. 31 | * 32 | * We need to allow all sorts of content, not just the ones the regular Response setContent() allows 33 | * 34 | * @param mixed $content 35 | * @return Response 36 | * @api 37 | */ 38 | public function setCustomContent(mixed $content): static 39 | { 40 | $this->customContent = $content; 41 | 42 | return $this; 43 | } 44 | 45 | public function getCustomContent(): mixed 46 | { 47 | return $this->customContent; 48 | } 49 | 50 | public function setContent(?string $content): static 51 | { 52 | $this->customContent = $content; 53 | return parent::setContent($content); 54 | } 55 | 56 | public function getContent(): string|false 57 | { 58 | return serialize($this->customContent); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Response/ResponseTransformer.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 55 | $this->responseModelFactory = $responseModelFactory; 56 | 57 | $this->setOptions($options); 58 | } 59 | 60 | /** 61 | * @param array $options 62 | */ 63 | public function setOptions(array $options) 64 | { 65 | if (isset($options['debug'])) { 66 | $this->setDebug($options['debug']); 67 | } 68 | if (isset($options['post_message_origin'])) { 69 | $this->setPostMessageOrigin($options['post_message_origin']); 70 | } 71 | } 72 | 73 | /** 74 | * @return boolean 75 | */ 76 | public function isDebug() 77 | { 78 | return $this->debug; 79 | } 80 | 81 | /** 82 | * @param boolean $debug 83 | * 84 | * @return ResponseTransformer 85 | */ 86 | public function setDebug($debug) 87 | { 88 | $this->debug = $debug; 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * @return string 95 | */ 96 | public function getPostMessageOrigin() 97 | { 98 | return $this->postMessageOrigin; 99 | } 100 | 101 | /** 102 | * @param string $postMessageOrigin 103 | * 104 | * @return ResponseTransformer 105 | */ 106 | public function setPostMessageOrigin($postMessageOrigin) 107 | { 108 | $this->postMessageOrigin = $postMessageOrigin; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * @param Request $request 115 | * @param SymfonyResponse $response 116 | * 117 | * @return SymfonyResponse 118 | */ 119 | public function transformEarly(Request $request, SymfonyResponse $response) 120 | { 121 | if ($response instanceof ExtendedResponseInterface) { 122 | $responseModel = $response->getCustomContent(); 123 | } else { 124 | $responseModel = $response->getContent(); 125 | } 126 | 127 | if (!$responseModel instanceof ResponseModelInterface) { 128 | $responseModel = $this->responseModelFactory->createFromContent( 129 | $response 130 | ); 131 | } 132 | 133 | $responseModel->setReturnStackTrace($this->isDebug()); 134 | $response->setStatusCode($responseModel->getStatusCode()); 135 | $this->forceStatusCodeHttpOK($request, $response, $responseModel); 136 | $response = $this->createSerializedResponse( 137 | $request, 138 | $response, 139 | $responseModel 140 | ); 141 | 142 | return $response; 143 | } 144 | 145 | public function transformLate(Request $request, SymfonyResponse $response) 146 | { 147 | if ($request->getRequestFormat() === Format::FORMAT_JSON 148 | && $request->query->has(self::PARAMETER_CALLBACK) 149 | && $response instanceof JsonResponse 150 | ) { 151 | $this->wrapResponse($request, $response); 152 | } 153 | 154 | $this->forceEmptyResponseOnHttpNoContent($response); 155 | } 156 | 157 | /** 158 | * @param $data 159 | * 160 | * @return Response 161 | */ 162 | public function createResponseFromContent($data) 163 | { 164 | return new Response( 165 | $this->responseModelFactory->createFromContent($data) 166 | ); 167 | } 168 | 169 | /** 170 | * Check if we should put the status code in the output and force a 200 OK 171 | * in the header 172 | * 173 | * @param Request $request 174 | * @param SymfonyResponse $response 175 | * @param ResponseModelInterface $responseModel 176 | */ 177 | protected function forceStatusCodeHttpOK( 178 | Request $request, 179 | SymfonyResponse $response, 180 | ResponseModelInterface $responseModel 181 | ) { 182 | if ($request->headers->has('X-Force-Status-Code-200') 183 | || ($request->getRequestFormat( 184 | ) == Format::FORMAT_JSON && $request->query->has( 185 | self::PARAMETER_CALLBACK 186 | )) 187 | ) { 188 | $responseModel->setReturnStatusCode(true); 189 | $response->setStatusCode(Response::HTTP_OK); 190 | $response->headers->set('X-Status-Code', Response::HTTP_OK); 191 | } 192 | } 193 | 194 | /** 195 | * Make sure content is empty when the status code is "204 NoContent" 196 | * 197 | * @param SymfonyResponse $response 198 | */ 199 | protected function forceEmptyResponseOnHttpNoContent( 200 | SymfonyResponse $response 201 | ) { 202 | if ($response->getStatusCode() === Response::HTTP_NO_CONTENT) { 203 | $response->setContent(null); 204 | if ($response instanceof ExtendedResponseInterface) { 205 | $response->setCustomContent(null); 206 | } 207 | $response->headers->remove('Content-Type'); 208 | } 209 | } 210 | 211 | /** 212 | * @param Request $request 213 | * @param SymfonyResponse $response 214 | * @param ResponseModelInterface $responseModel 215 | * 216 | * @return SymfonyResponse 217 | */ 218 | protected function createSerializedResponse( 219 | Request $request, 220 | SymfonyResponse $response, 221 | ResponseModelInterface $responseModel 222 | ) { 223 | try { 224 | $response = $this->serialize($request, $response, $responseModel); 225 | } catch (\Exception $e) { 226 | $response = new SymfonyJsonResponse( 227 | [ 228 | 'error' => [ 229 | 'code' => Error::CODE_GENERAL, 230 | 'message' => $e->getMessage(), 231 | ], 232 | ] 233 | ); 234 | } 235 | 236 | return $response; 237 | } 238 | 239 | /** 240 | * @param Request $request 241 | * @param SymfonyResponse $response 242 | * @param ResponseModelInterface $responseModel 243 | * 244 | * @return JsonResponse|SymfonyResponse 245 | */ 246 | protected function serialize( 247 | Request $request, 248 | SymfonyResponse $response, 249 | ResponseModelInterface $responseModel 250 | ) { 251 | switch ($request->getRequestFormat()) { 252 | case Format::FORMAT_XML: 253 | $response->setContent( 254 | $this->getSerializedContent($request, $responseModel) 255 | ); 256 | break; 257 | default: 258 | $headers = $response->headers; 259 | $response = new JsonResponse( 260 | $this->getSerializedContent($request, $responseModel), 261 | $response->getStatusCode() 262 | ); 263 | $response->headers = $headers; // some headers might mess up if we pass it to the JsonResponse 264 | break; 265 | } 266 | 267 | return $response; 268 | } 269 | 270 | /** 271 | * @param Request $request 272 | * @param ResponseModelInterface $responseModel 273 | * 274 | * @return mixed|string 275 | */ 276 | protected function getSerializedContent( 277 | Request $request, 278 | ResponseModelInterface $responseModel 279 | ) { 280 | return $this->serializer->serialize( 281 | $responseModel->toArray(), 282 | $request->getRequestFormat() 283 | ); 284 | } 285 | 286 | /** 287 | * @param Request $request 288 | * @param JsonResponse $response 289 | * 290 | * @throws \Exception 291 | */ 292 | protected function wrapResponse(Request $request, JsonResponse $response) 293 | { 294 | switch ($request->query->get(self::PARAMETER_WRAPPER)) { 295 | case self::WRAPPER_POST_MESSAGE: 296 | $response->setContent( 297 | sprintf( 298 | $this->getPostMessageTemplate(), 299 | $response->getContent(), 300 | $this->getCallbackFromRequest($request), 301 | $this->getPostMessageOrigin() 302 | ) 303 | )->headers->set('Content-Type', 'text/html'); 304 | break; 305 | default: 306 | $response->setCallback( 307 | $request->query->get(self::PARAMETER_CALLBACK) 308 | ); 309 | break; 310 | } 311 | } 312 | 313 | /** 314 | * @param Request $request 315 | * 316 | * @return string 317 | */ 318 | protected function getCallbackFromRequest(Request $request) 319 | { 320 | $response = new JsonResponse(''); 321 | $response->setCallback($request->query->get(self::PARAMETER_CALLBACK)); 322 | 323 | return $response->getCallback(); 324 | } 325 | 326 | /** 327 | * @return string 328 | */ 329 | protected function getPostMessageTemplate() 330 | { 331 | return << 333 | 334 | 347 | 348 | 349 | EOD; 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/Response/ResponseTransformerInterface.php: -------------------------------------------------------------------------------- 1 | serializers[] = $serializer; 26 | $this->formats = array_merge($this->formats, $serializer->getSupportedFormats()); 27 | } 28 | 29 | /** 30 | * @throws SerializerException 31 | */ 32 | public function serialize(mixed $data, string $format): string 33 | { 34 | $this->assertHasSerializer(); 35 | 36 | foreach ($this->serializers as $serializer) { 37 | if ($serializer->supportsFormat($format)) { 38 | return $serializer->serialize($data, $format); 39 | } 40 | } 41 | 42 | throw new SerializerException(sprintf('No serializer found to support format "%s"', $format)); 43 | } 44 | 45 | /** 46 | * @throws SerializerException 47 | */ 48 | public function getSupportedFormats(): array 49 | { 50 | $this->assertHasSerializer(); 51 | 52 | return $this->formats; 53 | } 54 | 55 | /** 56 | * @throws SerializerException 57 | */ 58 | public function getDefaultFormat(): string 59 | { 60 | $this->assertHasSerializer(); 61 | 62 | return $this->serializers[0]->getDefaultFormat(); 63 | } 64 | 65 | /** 66 | * @throws SerializerException 67 | */ 68 | private function assertHasSerializer() 69 | { 70 | if (count($this->serializers) === 0) { 71 | throw new SerializerException('No serializer was configured'); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Serializer/JMSSerializer.php: -------------------------------------------------------------------------------- 1 | serializer->serialize($data, $format, $this->context); 21 | } 22 | 23 | public function getSupportedFormats(): array 24 | { 25 | return [Format::FORMAT_JSON, Format::FORMAT_XML]; 26 | } 27 | 28 | public function getDefaultFormat(): string 29 | { 30 | return Format::FORMAT_JSON; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Serializer/JsonSerializer.php: -------------------------------------------------------------------------------- 1 | getSupportedFormats()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Util/StringUtil.php: -------------------------------------------------------------------------------- 1 | getShortName(); 16 | if (!is_null($trim)) { 17 | $name = str_replace($trim, '', $name); 18 | } 19 | 20 | return ltrim(strtolower(preg_replace('/[A-Z]/', '_$0', $name)), '_'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/EventSubscriber/RestApiEventSubscriberFactoryTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriber', $eventSubscriber); 14 | } 15 | 16 | public function testCreateWithOptions() 17 | { 18 | $eventSubscriber = RestApiEventSubscriberFactory::create( 19 | [ 20 | 'debug' => true, 21 | 'post_message_origin' => 'https://www.mediamonks.com' 22 | ] 23 | ); 24 | $this->assertInstanceOf('MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriber', $eventSubscriber); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/EventSubscriber/RestApiEventSubscriberTest.php: -------------------------------------------------------------------------------- 1 | getMocks(); 28 | 29 | return new RestApiEventSubscriber( 30 | $matcher, 31 | $requestTransformer, 32 | $responseTransformer 33 | ); 34 | } 35 | 36 | protected function getMocks() 37 | { 38 | $matcher = m::mock(RequestMatcherInterface::class); 39 | $requestTransformer = m::mock(RequestTransformerInterface::class); 40 | $responseTransformer = m::mock(ResponseTransformerInterface::class); 41 | $responseTransformer->shouldReceive('createResponseFromContent') 42 | ->andReturn(new Response()); 43 | 44 | return [$matcher, $requestTransformer, $responseTransformer]; 45 | } 46 | 47 | public function testGetSubscribedEvents() 48 | { 49 | $subscribedEvents = RestApiEventSubscriber::getSubscribedEvents(); 50 | 51 | $this->assertArrayHasKey(KernelEvents::REQUEST, $subscribedEvents); 52 | $this->assertArrayHasKey(KernelEvents::EXCEPTION, $subscribedEvents); 53 | $this->assertArrayHasKey(KernelEvents::VIEW, $subscribedEvents); 54 | $this->assertArrayHasKey(KernelEvents::RESPONSE, $subscribedEvents); 55 | } 56 | 57 | public function testOnRequestIsBound() 58 | { 59 | $this->methodIsBound('onRequest', KernelEvents::REQUEST); 60 | } 61 | 62 | public function testOnRequestNoMatch() 63 | { 64 | [$matcher, $requestTransformer, $responseTransformer] = $this->getMocks( 65 | ); 66 | $matcher->shouldReceive('matches')->andReturn(false); 67 | $requestTransformer->shouldReceive('transform'); 68 | 69 | $subject = $this->getSubject( 70 | [$matcher, $requestTransformer, $responseTransformer] 71 | ); 72 | 73 | $request = m::mock(Request::class); 74 | $request->shouldReceive('getMethod')->andReturn(Request::METHOD_GET); 75 | 76 | $mockEvent = m::mock(RequestEvent::class); 77 | $mockEvent->shouldReceive('getRequest')->andReturn($request); 78 | $mockEvent->shouldReceive('getRequestType'); 79 | 80 | $subject->onRequest($mockEvent); 81 | 82 | try { 83 | $requestTransformer->shouldNotHaveReceived('transform'); 84 | $this->assertTrue(true); 85 | } catch (\Exception $e) { 86 | $this->assertTrue(false, $e->getMessage()); 87 | } 88 | } 89 | 90 | public function testOnRequest() 91 | { 92 | [$matcher, $requestTransformer, $responseTransformer] = $this->getMocks( 93 | ); 94 | $matcher->shouldReceive('matches')->andReturn(true); 95 | $requestTransformer->shouldReceive('transform'); 96 | 97 | $subject = $this->getSubject( 98 | [$matcher, $requestTransformer, $responseTransformer] 99 | ); 100 | 101 | $kernel = m::mock(HttpKernel::class); 102 | $request = m::mock(Request::class); 103 | $request->shouldReceive('getMethod')->andReturn(Request::METHOD_GET); 104 | 105 | $event = new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); 106 | 107 | $subject->onRequest($event); 108 | 109 | try { 110 | $requestTransformer->shouldHaveReceived('transform')->between(1, 1); 111 | $this->assertTrue(true); 112 | } catch (\Exception $e) { 113 | $this->assertTrue(false, $e->getMessage()); 114 | } 115 | } 116 | 117 | public function testOnRequestOptionMethod() 118 | { 119 | [$matcher, $requestTransformer, $responseTransformer] = $this->getMocks(); 120 | $matcher->shouldReceive('matches')->andReturn(true); 121 | $requestTransformer->shouldReceive('transform'); 122 | 123 | $subject = $this->getSubject( 124 | [$matcher, $requestTransformer, $responseTransformer] 125 | ); 126 | 127 | $kernel = m::mock(HttpKernel::class); 128 | $request = m::mock(Request::class); 129 | 130 | $request->shouldReceive('setMethod')->andReturn(Request::METHOD_OPTIONS); 131 | $request->shouldReceive('getMethod')->andReturn(Request::METHOD_OPTIONS); 132 | 133 | $request->setMethod(Request::METHOD_OPTIONS); 134 | 135 | $event = new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); 136 | 137 | $subject->onRequest($event); 138 | 139 | try { 140 | $requestTransformer->shouldNotHaveReceived('transform'); 141 | $this->assertTrue(true); 142 | } catch (\Exception $e) { 143 | $this->assertTrue(false, $e->getMessage()); 144 | } 145 | } 146 | 147 | public function testOnExceptionIsBound() 148 | { 149 | $this->methodIsBound('onException', KernelEvents::EXCEPTION); 150 | } 151 | 152 | public function testOnExceptionNoMatch() 153 | { 154 | [$matcher, $requestTransformer, $responseTransformer] = $this->getMocks( 155 | ); 156 | $matcher->shouldReceive('matches')->andReturn(false); 157 | 158 | $subject = $this->getSubject( 159 | [$matcher, $requestTransformer, $responseTransformer] 160 | ); 161 | 162 | $kernel = m::mock(HttpKernel::class); 163 | $request = m::mock(Request::class); 164 | $request->shouldReceive('getMethod')->andReturn(Request::METHOD_GET); 165 | 166 | $e = new \Exception(); 167 | 168 | $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $e); 169 | $subject->onException($event); 170 | 171 | try { 172 | 173 | $this->assertTrue(true); 174 | } catch (\Exception $e) { 175 | $this->assertTrue(false, $e->getMessage()); 176 | } 177 | } 178 | 179 | public function testOnException() 180 | { 181 | [$matcher, $requestTransformer, $responseTransformer] = $this->getMocks( 182 | ); 183 | $matcher->shouldReceive('matches')->andReturn(true); 184 | 185 | $subject = $this->getSubject( 186 | [$matcher, $requestTransformer, $responseTransformer] 187 | ); 188 | 189 | $kernel = m::mock(HttpKernel::class); 190 | $request = m::mock(Request::class); 191 | $request->shouldReceive('getMethod')->andReturn(Request::METHOD_GET); 192 | 193 | $e = new \Exception(); 194 | 195 | $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $e); 196 | $subject->onException($event); 197 | 198 | try { 199 | $this->assertNotEmpty($event->getResponse()); 200 | $this->assertTrue(true); 201 | } catch (\Exception $e) { 202 | $this->assertTrue(false, $e->getMessage()); 203 | } 204 | } 205 | 206 | public function testOnViewIsBould() 207 | { 208 | $this->methodIsBound('onView', KernelEvents::VIEW); 209 | } 210 | 211 | public function testOnViewNoMatch() 212 | { 213 | [$matcher, $requestTransformer, $responseTransformer] = $this->getMocks( 214 | ); 215 | $matcher->shouldReceive('matches')->andReturn(false); 216 | 217 | $subject = $this->getSubject( 218 | [$matcher, $requestTransformer, $responseTransformer] 219 | ); 220 | 221 | $kernel = m::mock(HttpKernel::class); 222 | $request = m::mock(Request::class); 223 | $request->shouldReceive('getMethod')->andReturn(Request::METHOD_GET); 224 | 225 | $event = new ViewEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, 'foo'); 226 | 227 | $subject->onView($event); 228 | 229 | try { 230 | $this->assertEmpty($event->getResponse()); 231 | $this->assertTrue(true); 232 | } catch (\Exception $e) { 233 | $this->assertTrue(false, $e->getMessage()); 234 | } 235 | } 236 | 237 | public function testOnView() 238 | { 239 | [$matcher, $requestTransformer, $responseTransformer] = $this->getMocks( 240 | ); 241 | $matcher->shouldReceive('matches')->andReturn(true); 242 | 243 | $subject = $this->getSubject( 244 | [$matcher, $requestTransformer, $responseTransformer] 245 | ); 246 | 247 | $kernel = m::mock(HttpKernel::class); 248 | $request = m::mock(Request::class); 249 | $request->shouldReceive('getMethod')->andReturn(Request::METHOD_GET); 250 | 251 | $event = new ViewEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, 'foo'); 252 | 253 | $subject->onView($event); 254 | 255 | try { 256 | $this->assertNotEmpty($event->getResponse()); 257 | $this->assertTrue(true); 258 | } catch (\Exception $e) { 259 | $this->assertTrue(false, $e->getMessage()); 260 | } 261 | } 262 | 263 | public function testOnResponseEarlyIsBound() 264 | { 265 | $this->methodIsBound('onResponseEarly', KernelEvents::RESPONSE); 266 | } 267 | 268 | public function testOnResponseEarlyNoMatch() 269 | { 270 | [$matcher, $requestTransformer, $responseTransformer] = $this->getMocks( 271 | ); 272 | $matcher->shouldReceive('matches')->andReturn(false); 273 | 274 | $subject = $this->getSubject( 275 | [$matcher, $requestTransformer, $responseTransformer] 276 | ); 277 | 278 | $kernel = m::mock(HttpKernel::class); 279 | $request = m::mock(Request::class); 280 | $request->shouldReceive('getMethod')->andReturn(Request::METHOD_GET); 281 | $response = m::mock(Response::class); 282 | 283 | $event = new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response); 284 | 285 | $subject->onResponseEarly($event); 286 | 287 | try { 288 | //$mockEvent->shouldNotHaveReceived('setResponse'); 289 | $this->assertTrue(true); 290 | } catch (\Exception $e) { 291 | $this->assertTrue(false, $e->getMessage()); 292 | } 293 | } 294 | 295 | public function testOnResponseEarly() 296 | { 297 | $response = new \Symfony\Component\HttpFoundation\Response(); 298 | 299 | [$matcher, $requestTransformer, $responseTransformer] = $this->getMocks(); 300 | $matcher->shouldReceive('matches')->andReturn(true); 301 | $responseTransformer->shouldReceive('transformEarly')->andReturn($response); 302 | 303 | $subject = $this->getSubject( 304 | [$matcher, $requestTransformer, $responseTransformer] 305 | ); 306 | 307 | $kernel = m::mock(HttpKernel::class); 308 | $request = m::mock(Request::class); 309 | $request->shouldReceive('getMethod')->andReturn(Request::METHOD_GET); 310 | 311 | $event = new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response); 312 | 313 | $subject->onResponseEarly($event); 314 | 315 | try { 316 | $this->assertEquals($response, $event->getResponse()); 317 | } catch (\Exception $e) { 318 | $this->assertTrue(false, $e->getMessage()); 319 | } 320 | } 321 | 322 | public function testOnResponseLateIsBound() 323 | { 324 | $this->methodIsBound('onResponseLate', KernelEvents::RESPONSE); 325 | } 326 | 327 | public function testOnResponseLateNoMatch() 328 | { 329 | [$matcher, $requestTransformer, $responseTransformer] = $this->getMocks( 330 | ); 331 | $matcher->shouldReceive('matches')->andReturn(false); 332 | 333 | $subject = $this->getSubject( 334 | [$matcher, $requestTransformer, $responseTransformer] 335 | ); 336 | 337 | $kernel = m::mock(HttpKernel::class); 338 | $request = m::mock(Request::class); 339 | $request->shouldReceive('getMethod')->andReturn(Request::METHOD_GET); 340 | $response = m::mock(Response::class); 341 | 342 | $event = new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response); 343 | $subject->onResponseLate($event); 344 | 345 | try { 346 | $requestTransformer->shouldNotHaveReceived('transformLate'); 347 | $this->assertTrue(true); 348 | } catch (\Exception $e) { 349 | $this->assertTrue(false, $e->getMessage()); 350 | } 351 | } 352 | 353 | public function testOnResponseLate() 354 | { 355 | [$matcher, $requestTransformer, $responseTransformer] = $this->getMocks( 356 | ); 357 | $matcher->shouldReceive('matches')->andReturn(true); 358 | $responseTransformer->shouldReceive('transformLate'); 359 | 360 | $subject = $this->getSubject( 361 | [$matcher, $requestTransformer, $responseTransformer] 362 | ); 363 | 364 | $kernel = m::mock(HttpKernel::class); 365 | $request = m::mock(Request::class); 366 | $request->shouldReceive('getMethod')->andReturn(Request::METHOD_GET); 367 | $response = m::mock(Response::class); 368 | 369 | $event = new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response); 370 | 371 | $subject->onResponseLate($event); 372 | 373 | try { 374 | $responseTransformer->shouldHaveReceived('transformLate')->between( 375 | 1, 376 | 1 377 | ); 378 | $this->assertTrue(true); 379 | } catch (\Exception $e) { 380 | $this->assertTrue(false, $e->getMessage()); 381 | } 382 | } 383 | 384 | protected function methodIsBound($method, $testEvent) 385 | { 386 | foreach ( 387 | RestApiEventSubscriber::getSubscribedEvents() as $event => 388 | $listeners 389 | ) { 390 | foreach ($listeners as $listener) { 391 | [$listener] = $listener; 392 | if ($listener == $method && $event == $testEvent) { 393 | $this->assertTrue(true); 394 | 395 | return; 396 | } 397 | } 398 | } 399 | 400 | $this->assertTrue(false, $method.' is not bound to event '.$testEvent); 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /tests/Exception/ErrorFieldTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('field', $errorField->getField()); 14 | $this->assertEquals('code', $errorField->getCode()); 15 | $this->assertEquals('message', $errorField->getMessage()); 16 | $this->assertEquals([ 17 | 'field' => 'field', 18 | 'code' => 'code', 19 | 'message' => 'message' 20 | ], $errorField->toArray()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Exception/FormValidationExceptionTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getErrors')->andReturn(new FormErrorIterator($form, [])); 20 | $form->shouldReceive('all')->andReturn([]); 21 | 22 | $exception = new FormValidationException($form); 23 | 24 | $arrayException = $exception->toArray(); 25 | 26 | $this->assertEquals(400, $arrayException['code']); 27 | $this->assertEquals('Not all fields are filled in correctly.', $arrayException['message']); 28 | } 29 | 30 | public function testEmptyFormToArrayCustom() 31 | { 32 | $form = m::mock('Symfony\Component\Form\FormInterface'); 33 | $form->shouldReceive('getErrors')->andReturn(new FormErrorIterator($form, [])); 34 | $form->shouldReceive('all')->andReturn([]); 35 | 36 | $exception = new FormValidationException($form, 'my_message', 500); 37 | 38 | $arrayException = $exception->toArray(); 39 | 40 | $this->assertEquals(500, $arrayException['code']); 41 | $this->assertEquals('my_message', $arrayException['message']); 42 | } 43 | 44 | public function testToArray() 45 | { 46 | $form = m::mock('Symfony\Component\Form\FormInterface'); 47 | $form->shouldReceive('isRoot')->andReturn(true); 48 | $form->shouldReceive('getErrors')->andReturn(new FormErrorIterator($form, [ 49 | new FormError('General Error'), 50 | new FormError('CSRF Error'), 51 | ])); 52 | 53 | $childForm = m::mock('Symfony\Component\Form\FormInterface'); 54 | $childForm->shouldReceive('isRoot')->andReturn(false); 55 | $childForm->shouldReceive('isValid')->andReturn(false); 56 | $childForm->shouldReceive('getName')->andReturn('name'); 57 | $childForm->shouldReceive('all')->andReturn([]); 58 | $childForm->shouldReceive('getErrors')->andReturn(new FormErrorIterator($form, [ 59 | new FormError('Constraint Failed Error', '', [], null, new ConstraintViolation( 60 | 'Foo', '', [], false, 'name', null, null, null, new NotBlank() 61 | )), 62 | new FormError('Other Error'), 63 | ])); 64 | 65 | $form->shouldReceive('all')->andReturn([ 66 | $childForm, 67 | ]); 68 | 69 | $exception = new FormValidationException($form); 70 | 71 | $arrayException = $exception->toArray(); 72 | 73 | $this->assertEquals('#', $arrayException['fields'][0]['field']); 74 | $this->assertEquals(Error::ERROR_KEY_FORM_VALIDATION . '.general', $arrayException['fields'][0]['code']); 75 | $this->assertEquals('General Error', $arrayException['fields'][0]['message']); 76 | 77 | $this->assertEquals('#', $arrayException['fields'][1]['field']); 78 | $this->assertEquals(Error::ERROR_KEY_FORM_VALIDATION . '.csrf', $arrayException['fields'][1]['code']); 79 | $this->assertEquals('CSRF Error', $arrayException['fields'][1]['message']); 80 | 81 | $this->assertEquals('name', $arrayException['fields'][2]['field']); 82 | $this->assertEquals(Error::ERROR_KEY_FORM_VALIDATION . '.not_blank', $arrayException['fields'][2]['code']); 83 | $this->assertEquals('Constraint Failed Error', $arrayException['fields'][2]['message']); 84 | 85 | $this->assertEquals('name', $arrayException['fields'][3]['field']); 86 | $this->assertEquals(Error::ERROR_KEY_FORM_VALIDATION . '.general', $arrayException['fields'][3]['code']); 87 | $this->assertEquals('Other Error', $arrayException['fields'][3]['message']); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Exception/JsonSerializableException.php: -------------------------------------------------------------------------------- 1 | 0, 'message' => 'json_serialized_message', 'fields' => []]; 13 | } 14 | } -------------------------------------------------------------------------------- /tests/Exception/ValidationExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertCount(1, $validationException->getFields()); 21 | $this->assertEquals($error, $validationException->getFields()[0]); 22 | } 23 | 24 | public function testFailOnInvalidFields() 25 | { 26 | $this->expectException('InvalidArgumentException'); 27 | new ValidationException(['foo']); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Form/Type/TestType.php: -------------------------------------------------------------------------------- 1 | =')) { 17 | $textField = 'Symfony\Component\Form\Extension\Core\Type\TextType'; 18 | } 19 | 20 | $builder 21 | ->add( 22 | 'name', 23 | $textField, 24 | [ 25 | 'constraints' => [ 26 | new Constraint\NotBlank, 27 | new Constraint\Length(['min' => 3, 'max' => 255]) 28 | ] 29 | ] 30 | ) 31 | ->add( 32 | 'email', 33 | $textField, 34 | [ 35 | 'constraints' => [ 36 | new Constraint\NotBlank, 37 | new Constraint\Email, 38 | new Constraint\Length(['min' => 5, 'max' => 255]) 39 | ] 40 | ] 41 | ) 42 | ; 43 | } 44 | 45 | public function configureOptions(OptionsResolver $resolver) 46 | { 47 | $resolver->setDefaults( 48 | [ 49 | 'csrf_protection' => false 50 | ] 51 | ); 52 | } 53 | 54 | public function getName() 55 | { 56 | return ''; 57 | } 58 | } -------------------------------------------------------------------------------- /tests/Model/ResponseModelFactoryTest.php: -------------------------------------------------------------------------------- 1 | createResponseModel($exception); 20 | 21 | $this->assertEquals(Response::HTTP_INTERNAL_SERVER_ERROR, $responseContainer->getStatusCode()); 22 | $this->assertNull($responseContainer->getData()); 23 | $this->assertEquals($exception, $responseContainer->getThrowable()); 24 | $this->assertNull($responseContainer->getResponse()); 25 | $this->assertNull($responseContainer->getExtendedResponse()); 26 | $this->assertNull($responseContainer->getPagination()); 27 | $this->assertFalse($responseContainer->isEmpty()); 28 | 29 | $responseContainerArray = $responseContainer->toArray(); 30 | 31 | $this->assertArrayHasKey('error', $responseContainerArray); 32 | $this->assertEquals('error', $responseContainerArray['error']['code']); 33 | $this->assertEquals('foo', $responseContainerArray['error']['message']); 34 | } 35 | 36 | public function testAutoDetectHttpException() 37 | { 38 | $notFoundHttpException = new NotFoundHttpException('foo'); 39 | $responseContainer = $this->createResponseModel($notFoundHttpException); 40 | 41 | $this->assertEquals(Response::HTTP_NOT_FOUND, $responseContainer->getStatusCode()); 42 | $this->assertNull($responseContainer->getData()); 43 | $this->assertEquals($notFoundHttpException, $responseContainer->getThrowable()); 44 | $this->assertNull($responseContainer->getResponse()); 45 | $this->assertNull($responseContainer->getExtendedResponse()); 46 | $this->assertNull($responseContainer->getPagination()); 47 | $this->assertFalse($responseContainer->isEmpty()); 48 | 49 | $responseContainerArray = $responseContainer->toArray(); 50 | 51 | $this->assertArrayHasKey('error', $responseContainerArray); 52 | $this->assertEquals($responseContainerArray['error']['code'], 'error.http.not_found'); 53 | $this->assertEquals($responseContainerArray['error']['message'], 'foo'); 54 | } 55 | 56 | public function testAutoDetectPaginatedResponse() 57 | { 58 | $paginatedResponse = new OffsetPaginatedResponse('foo', 1, 2, 3); 59 | $responseContainer = $this->createResponseModel($paginatedResponse); 60 | 61 | $this->assertEquals(Response::HTTP_OK, $responseContainer->getStatusCode()); 62 | $this->assertIsString($responseContainer->getData()); 63 | $this->assertNull($responseContainer->getThrowable()); 64 | $this->assertNull($responseContainer->getResponse()); 65 | $this->assertNull($responseContainer->getExtendedResponse()); 66 | $this->assertEquals($paginatedResponse, $responseContainer->getPagination()); 67 | $this->assertFalse($responseContainer->isEmpty()); 68 | 69 | $responseContainerArray = $responseContainer->toArray(); 70 | $this->assertArrayHasKey('data', $responseContainerArray); 71 | $this->assertArrayHasKey('pagination', $responseContainerArray); 72 | } 73 | 74 | public function testAutoDetectEmptyResponse() 75 | { 76 | $responseContainer = $this->createResponseModel(null); 77 | $this->assertEquals(Response::HTTP_NO_CONTENT, $responseContainer->getStatusCode()); 78 | $this->assertNull($responseContainer->getData()); 79 | $this->assertNull($responseContainer->getThrowable()); 80 | $this->assertNull($responseContainer->getResponse()); 81 | $this->assertNull($responseContainer->getExtendedResponse()); 82 | $this->assertNull($responseContainer->getPagination()); 83 | $this->assertTrue($responseContainer->isEmpty()); 84 | } 85 | 86 | public function testAutoDetectEmptyExtendedResponse() 87 | { 88 | $responseContainer = $this->createResponseModel(null); 89 | $this->assertEquals(Response::HTTP_NO_CONTENT, $responseContainer->getStatusCode()); 90 | $this->assertNull($responseContainer->getData()); 91 | $this->assertNull($responseContainer->getThrowable()); 92 | $this->assertNull($responseContainer->getResponse()); 93 | $this->assertNull($responseContainer->getExtendedResponse()); 94 | $this->assertNull($responseContainer->getPagination()); 95 | $this->assertTrue($responseContainer->isEmpty()); 96 | } 97 | 98 | public function testAutoDetectStringResponse() 99 | { 100 | $stringData = 'foo'; 101 | $responseContainer = $this->createResponseModel($stringData); 102 | 103 | $this->assertEquals(Response::HTTP_OK, $responseContainer->getStatusCode()); 104 | $this->assertIsString($responseContainer->getData()); 105 | $this->assertNull($responseContainer->getThrowable()); 106 | $this->assertNull($responseContainer->getResponse()); 107 | $this->assertNull($responseContainer->getExtendedResponse()); 108 | $this->assertNull($responseContainer->getPagination()); 109 | $this->assertFalse($responseContainer->isEmpty()); 110 | } 111 | 112 | public function testAutoDetectArrayResponse() 113 | { 114 | $arrayData = ['foo', 'bar']; 115 | $responseContainer = $this->createResponseModel($arrayData); 116 | 117 | $this->assertEquals(Response::HTTP_OK, $responseContainer->getStatusCode()); 118 | $this->assertIsArray($responseContainer->getData()); 119 | $this->assertNull($responseContainer->getThrowable()); 120 | $this->assertNull($responseContainer->getResponse()); 121 | $this->assertNull($responseContainer->getExtendedResponse()); 122 | $this->assertNull($responseContainer->getPagination()); 123 | $this->assertFalse($responseContainer->isEmpty()); 124 | } 125 | 126 | public function testAutoDetectRedirectResponse() 127 | { 128 | $uri = 'http://www.mediamonks.com'; 129 | $redirect = new RedirectResponse($uri, Response::HTTP_MOVED_PERMANENTLY); 130 | $responseContainer = $this->createResponseModel($redirect); 131 | 132 | $this->assertEquals(Response::HTTP_MOVED_PERMANENTLY, $responseContainer->getStatusCode()); 133 | $this->assertNull($responseContainer->getThrowable()); 134 | $this->assertEquals($redirect, $responseContainer->getResponse()); 135 | $this->assertNull($responseContainer->getExtendedResponse()); 136 | $this->assertNull($responseContainer->getPagination()); 137 | $this->assertFalse($responseContainer->isEmpty()); 138 | 139 | $data = $responseContainer->toArray(); 140 | 141 | $this->assertEquals($uri, $data['location']); 142 | } 143 | 144 | public function testAutoDetectSymfonyResponse() 145 | { 146 | $data = 'foo'; 147 | $response = new Response($data); 148 | $responseContainer = $this->createResponseModel($response); 149 | 150 | $this->assertEquals(Response::HTTP_OK, $responseContainer->getStatusCode()); 151 | $this->assertEquals($data, $responseContainer->getData()); 152 | $this->assertNull($responseContainer->getThrowable()); 153 | $this->assertEquals($response, $responseContainer->getResponse()); 154 | $this->assertNull($responseContainer->getExtendedResponse()); 155 | $this->assertNull($responseContainer->getPagination()); 156 | $this->assertFalse($responseContainer->isEmpty()); 157 | } 158 | 159 | public function testAutoDetectMediaMonksResponse() 160 | { 161 | $data = 'foo'; 162 | $response = new \MediaMonks\RestApi\Response\Response($data); 163 | $responseContainer = $this->createResponseModel($response); 164 | 165 | $this->assertEquals(Response::HTTP_OK, $responseContainer->getStatusCode()); 166 | $this->assertEquals($data, $responseContainer->getData()); 167 | $this->assertNull($responseContainer->getThrowable()); 168 | $this->assertNull($responseContainer->getResponse()); 169 | $this->assertEquals($response, $responseContainer->getExtendedResponse()); 170 | $this->assertNull($responseContainer->getPagination()); 171 | $this->assertFalse($responseContainer->isEmpty()); 172 | } 173 | 174 | public function testAutoDetectValidationExceptionResponse() 175 | { 176 | $exception = new ValidationException([]); 177 | $responseContainer = $this->createResponseModel($exception); 178 | 179 | $this->assertEquals(Response::HTTP_BAD_REQUEST, $responseContainer->getStatusCode()); 180 | $this->assertNull($responseContainer->getData()); 181 | $this->assertEquals($exception, $responseContainer->getThrowable()); 182 | $this->assertNull($responseContainer->getResponse()); 183 | $this->assertNull($responseContainer->getExtendedResponse()); 184 | $this->assertNull($responseContainer->getPagination()); 185 | $this->assertFalse($responseContainer->isEmpty()); 186 | } 187 | 188 | protected function createResponseModel(mixed $content): ResponseModel 189 | { 190 | $responseModelFactory = new ResponseModelFactory(new ResponseModel()); 191 | 192 | return $responseModelFactory->createFromContent($content); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /tests/Model/ResponseModelTest.php: -------------------------------------------------------------------------------- 1 | setData($data); 21 | $this->assertEquals($data, $responseContainer->getData()); 22 | } 23 | 24 | public function testExceptionGettersSetter() 25 | { 26 | $exception = new \Exception; 27 | $responseContainer = new ResponseModel(); 28 | $responseContainer->setThrowable($exception); 29 | $this->assertEquals($exception, $responseContainer->getThrowable()); 30 | } 31 | 32 | public function testLocationGettersSetter() 33 | { 34 | $location = 'http://www.mediamonks.com'; 35 | $redirect = new RedirectResponse($location); 36 | $responseContainer = new ResponseModel(); 37 | $responseContainer->setResponse($redirect); 38 | $this->assertEquals($redirect, $responseContainer->getResponse()); 39 | } 40 | 41 | public function testExtendedResponsGetterSetter() 42 | { 43 | $response = new ExtendedResponse('OK'); 44 | $responseContainer = new ResponseModel(); 45 | $responseContainer->setExtendedResponse($response); 46 | $this->assertEquals($response, $responseContainer->getExtendedResponse()); 47 | } 48 | 49 | public function testPaginationGettersSetter() 50 | { 51 | $pagination = new OffsetPaginatedResponse('foo', 1, 2, 3); 52 | $responseContainer = new ResponseModel(); 53 | $responseContainer->setPagination($pagination); 54 | $this->assertEquals($pagination, $responseContainer->getPagination()); 55 | } 56 | 57 | public function testReturnStatusCodeGetterSetter() 58 | { 59 | $statusCode = Response::HTTP_NOT_MODIFIED; 60 | $responseContainer = new ResponseModel(); 61 | $responseContainer->setReturnStatusCode($statusCode); 62 | $this->assertEquals($statusCode, $responseContainer->getReturnStatusCode()); 63 | } 64 | 65 | public function testStatusCodeGetterSetter() 66 | { 67 | $statusCode = Response::HTTP_OK; 68 | $responseContainer = new ResponseModel(); 69 | $responseContainer->setData('OK'); 70 | $responseContainer->setStatusCode($statusCode); 71 | $this->assertEquals($statusCode, $responseContainer->getStatusCode()); 72 | } 73 | 74 | public function testGetCodeFromStatusCode() 75 | { 76 | $statusCode = Response::HTTP_BAD_REQUEST; 77 | $code = 400; 78 | $exception = new \Exception('', $code); 79 | 80 | $responseContainer = new ResponseModel(); 81 | $responseContainer->setStatusCode($statusCode); 82 | $responseContainer->setThrowable($exception); 83 | 84 | $this->assertEquals($code, $responseContainer->getStatusCode()); 85 | } 86 | 87 | public function testToArrayStatusCode() 88 | { 89 | $responseContainer = new ResponseModel(); 90 | $responseContainer->setData('foo'); 91 | $responseContainer->setReturnStatusCode(true); 92 | 93 | $result = $responseContainer->toArray(); 94 | $this->assertEquals(Response::HTTP_OK, $result['statusCode']); 95 | } 96 | 97 | public function testJsonSerializableException() 98 | { 99 | $error = ['code' => 0, 'message' => 'json_serialized_message', 'fields' => []]; 100 | 101 | $responseContainer = new ResponseModel(); 102 | $responseContainer->setThrowable(new JsonSerializableException()); 103 | 104 | $this->assertEquals(['error' => $error], $responseContainer->toArray()); 105 | } 106 | 107 | public function testValidationExceptionToArrayFormValidationException() 108 | { 109 | if (defined('HHVM_VERSION')) { 110 | $this->markTestSkipped('This test fails on HHVM, see issue #8'); 111 | } 112 | 113 | $error = ['code' => 0, 'message' => '', 'fields' => null]; 114 | 115 | $mockException = m::mock('\MediaMonks\RestApi\Exception\ValidationException, \MediaMonks\RestApi\Exception\ExceptionInterface'); 116 | $mockException->shouldReceive('toArray')->andReturn($error); 117 | $mockException->shouldReceive('getFields'); 118 | 119 | $responseContainer = new ResponseModel(); 120 | $responseContainer->setThrowable($mockException); 121 | 122 | $this->assertEquals(['error' => $error], $responseContainer->toArray()); 123 | } 124 | 125 | public function testReturnStackTraceEnabled() 126 | { 127 | $responseContainer = new ResponseModel(); 128 | $responseContainer->setThrowable(new \Exception('Test')); 129 | $responseContainer->setReturnStackTrace(true); 130 | 131 | $this->assertTrue($responseContainer->isReturnStackTrace()); 132 | 133 | $data = $responseContainer->toArray(); 134 | $this->assertArrayHasKey('error', $data); 135 | $this->assertArrayHasKey('stack_trace', $data['error']); 136 | } 137 | 138 | public function testReturnStackTraceDisabled() 139 | { 140 | $responseContainer = new ResponseModel(); 141 | $responseContainer->setThrowable(new \Exception('Test')); 142 | $responseContainer->setReturnStackTrace(false); 143 | 144 | $this->assertFalse($responseContainer->isReturnStackTrace()); 145 | 146 | $data = $responseContainer->toArray(); 147 | $this->assertArrayHasKey('error', $data); 148 | $this->assertArrayNotHasKey('stack_trace', $data['error']); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tests/Request/FormatTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('json', Format::getDefault()); 13 | } 14 | 15 | public function testAvailableFormats() 16 | { 17 | $availableFormats = Format::getAvailable(); 18 | $this->assertCount(2, $availableFormats); 19 | $this->assertContains('json', $availableFormats); 20 | $this->assertContains('xml', $availableFormats); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Request/PathRequestMatcherTest.php: -------------------------------------------------------------------------------- 1 | '/foo', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => true], 18 | ['path' => '/bar', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => false], 19 | ['path' => '/foo', 'type' => HttpKernelInterface::SUB_REQUEST, 'result' => false], 20 | ['path' => '/bar', 'type' => HttpKernelInterface::SUB_REQUEST, 'result' => false], 21 | ] as $test 22 | ) { 23 | $this->assertEquals($test['result'], 24 | $matcher->matches($this->getRequestFromPath($test['path']), $test['type'])); 25 | } 26 | } 27 | 28 | 29 | /** 30 | * @param string $path 31 | * @return Request 32 | */ 33 | protected function getRequestFromPath($path) 34 | { 35 | return Request::create($path); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Request/RegexRequestMatcherTest.php: -------------------------------------------------------------------------------- 1 | '/foo', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => false], 19 | ['path' => '/bar', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => false], 20 | ['path' => '/foo', 'type' => HttpKernelInterface::SUB_REQUEST, 'result' => false], 21 | ['path' => '/bar', 'type' => HttpKernelInterface::SUB_REQUEST, 'result' => false], 22 | ] as $test 23 | ) { 24 | $this->assertEquals($test['result'], 25 | $matcher->matches($this->getRequestFromPath($test['path']), $test['type'])); 26 | } 27 | } 28 | 29 | public function testMatchesWhitelist() 30 | { 31 | $matcher = new RegexRequestMatcher([ 32 | '~^/api$~', 33 | '~^/api/~' 34 | ]); 35 | foreach ([ 36 | ['path' => '/foo', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => false], 37 | ['path' => '/foo', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => false], 38 | ['path' => '/fapi', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => false], 39 | ['path' => '/api', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => true], 40 | ['path' => '/api', 'type' => HttpKernelInterface::SUB_REQUEST, 'result' => false], 41 | ['path' => '/api/', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => true], 42 | ['path' => '/api/', 'type' => HttpKernelInterface::SUB_REQUEST, 'result' => false], 43 | ['path' => '/api/foo', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => true], 44 | ['path' => '/api/doc', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => true], 45 | ] as $test 46 | ) { 47 | $this->assertEquals($test['result'], 48 | $matcher->matches($this->getRequestFromPath($test['path']), $test['type'])); 49 | } 50 | } 51 | 52 | public function testMatchesWhitelistBlacklist() 53 | { 54 | $matcher = new RegexRequestMatcher([ 55 | '~^/api$~', 56 | '~^/api/~' 57 | ], [ 58 | '~^/api/doc~' 59 | ]); 60 | foreach ([ 61 | ['path' => '/foo', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => false], 62 | ['path' => '/foo', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => false], 63 | ['path' => '/fapi', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => false], 64 | ['path' => '/api', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => true], 65 | ['path' => '/api', 'type' => HttpKernelInterface::SUB_REQUEST, 'result' => false], 66 | ['path' => '/api/', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => true], 67 | ['path' => '/api/', 'type' => HttpKernelInterface::SUB_REQUEST, 'result' => false], 68 | ['path' => '/api/foo', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => true], 69 | ['path' => '/api/doc', 'type' => HttpKernelInterface::MASTER_REQUEST, 'result' => false], 70 | ['path' => '/api/doc', 'type' => HttpKernelInterface::SUB_REQUEST, 'result' => false], 71 | ] as $test 72 | ) { 73 | $this->assertEquals($test['result'], 74 | $matcher->matches($this->getRequestFromPath($test['path']), $test['type'])); 75 | } 76 | } 77 | 78 | public function testMatchedRequestIsMarkedAsMatched() 79 | { 80 | $matcher = new RegexRequestMatcher(['~^/api$~']); 81 | $request = $this->getRequestFromPath('/api'); 82 | 83 | $this->assertEquals(true, $matcher->matches($request)); 84 | $this->assertTrue($request->attributes->has(AbstractRequestMatcher::ATTRIBUTE_MATCHED)); 85 | $this->assertEquals(true, $matcher->matches($request)); 86 | } 87 | 88 | public function testNonMatchedRequestIsNotMarkedAsMatched() 89 | { 90 | $matcher = new RegexRequestMatcher(['~^/api$~']); 91 | $request = $this->getRequestFromPath('/'); 92 | 93 | $this->assertEquals(false, $matcher->matches($request)); 94 | $this->assertFalse($request->attributes->has(AbstractRequestMatcher::ATTRIBUTE_MATCHED)); 95 | } 96 | 97 | public function testMatchedRequestIsNotMatchedTwice() 98 | { 99 | $matcher = new RegexRequestMatcher(['~^/api$~']); 100 | $request = $this->getRequestFromPath('/'); 101 | 102 | $this->assertEquals(false, $matcher->matches($request)); 103 | $this->assertFalse($request->attributes->has(AbstractRequestMatcher::ATTRIBUTE_MATCHED)); 104 | $this->assertEquals(false, $matcher->matches($request)); 105 | } 106 | 107 | public function testMatchesAlreadyMatched() 108 | { 109 | $subject = new RegexRequestMatcher(['~^/api$~']); 110 | $request = $this->getRequestFromPath('/api'); 111 | 112 | // First match, path 1 113 | $this->assertTrue($subject->matches($request)); 114 | // Second match, shortcut path 2 115 | $this->assertTrue($subject->matches($request)); 116 | } 117 | 118 | /** 119 | * @param string $path 120 | * @return Request 121 | */ 122 | protected function getRequestFromPath($path) 123 | { 124 | return Request::create($path); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/Request/RequestTransformerTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getSupportedFormats')->andReturn(['json', 'xml']); 20 | $serializer->shouldReceive('getDefaultFormat')->andReturn('json'); 21 | 22 | return new RequestTransformer($serializer); 23 | } 24 | 25 | public function testTransformChangesRequestParameters() 26 | { 27 | $subject = $this->getSubject(); 28 | $content = ['Hello', 'World!']; 29 | $request = $this->getRequest($content); 30 | 31 | $subject->transform($request); 32 | 33 | $this->assertEquals($content, iterator_to_array($request->request->getIterator())); 34 | } 35 | 36 | public function testTransformChangesRequestFormatDefault() 37 | { 38 | $subject = $this->getSubject(); 39 | $request = $this->getRequest([]); 40 | 41 | $subject->transform($request); 42 | 43 | $this->assertEquals('json', $request->getRequestFormat()); 44 | } 45 | 46 | public function testTransformChangesRequestFormatGiven() 47 | { 48 | $subject = $this->getSubject(); 49 | $request = $this->getRequest([]); 50 | $request->initialize(['_format' => 'xml']); 51 | 52 | $subject->transform($request); 53 | 54 | $this->assertEquals('xml', $request->getRequestFormat()); 55 | } 56 | 57 | public function testTransformChangesRequestFormatUnknown() 58 | { 59 | $subject = $this->getSubject(); 60 | $request = $this->getRequest([]); 61 | $request->initialize(['_format' => 'csv']); 62 | 63 | $subject->transform($request); 64 | 65 | $this->assertEquals(Format::getDefault(), $request->getRequestFormat()); 66 | } 67 | 68 | protected function getRequest($content) 69 | { 70 | $request = Request::create('/'); 71 | $request->initialize([], [], [], [], [], [], json_encode($content)); 72 | $request->headers->add(['Content-type' => 'application/json']); 73 | return $request; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Response/CursorPaginatedResponseTest.php: -------------------------------------------------------------------------------- 1 | createCursorPaginatedResponse(); 27 | $this->assertEquals(self::DATA, $response->getData()); 28 | $this->assertEquals(self::BEFORE, $response->getBefore()); 29 | $this->assertEquals(self::AFTER, $response->getAfter()); 30 | $this->assertEquals(self::LIMIT, $response->getLimit()); 31 | $this->assertEquals(self::TOTAL, $response->getTotal()); 32 | } 33 | 34 | public function testCursorPaginatedResponseGettersSetters() 35 | { 36 | $response = new CursorPaginatedResponse(null, 0, 0, 0, 0); 37 | 38 | $response->setData(self::DATA); 39 | $response->setBefore(self::BEFORE); 40 | $response->setAfter(self::AFTER); 41 | $response->setLimit(self::LIMIT); 42 | $response->setTotal(self::TOTAL); 43 | 44 | $this->assertEquals(self::DATA, $response->getData()); 45 | $this->assertEquals(self::BEFORE, $response->getBefore()); 46 | $this->assertEquals(self::AFTER, $response->getAfter()); 47 | $this->assertEquals(self::LIMIT, $response->getLimit()); 48 | $this->assertEquals(self::TOTAL, $response->getTotal()); 49 | } 50 | 51 | public function testCursorPaginatedResponseToArray() 52 | { 53 | $response = $this->createCursorPaginatedResponse(); 54 | $data = $response->toArray(); 55 | 56 | $this->assertEquals(self::BEFORE, $data['before']); 57 | $this->assertEquals(self::AFTER, $data['after']); 58 | $this->assertEquals(self::LIMIT, $data['limit']); 59 | $this->assertEquals(self::TOTAL, $data['total']); 60 | } 61 | 62 | public function testToArrayNullTotal() 63 | { 64 | $subject = new CursorPaginatedResponse(null, 1, 2, 3); 65 | 66 | $expected = ['before' => 1, 'after' => 2, 'limit' => 3]; 67 | $this->assertEquals($expected, $subject->toArray()); 68 | } 69 | 70 | public function testToArrayNotNullTotal() 71 | { 72 | $subject = new CursorPaginatedResponse(null, 1, 2, 3, 4); 73 | 74 | $expected = ['before' => 1, 'after' => 2, 'limit' => 3, 'total' => 4]; 75 | $this->assertEquals($expected, $subject->toArray()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Response/JsonResponseTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($data, $response->getCustomContent()); 15 | } 16 | 17 | public function testJsonResponseSetter() 18 | { 19 | $data = ['foo', 'bar']; 20 | $response = new JsonResponse(); 21 | $response->setData($data); 22 | $this->assertEquals($data, $response->getCustomContent()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Response/OffsetPaginatedResponseTest.php: -------------------------------------------------------------------------------- 1 | createOffsetPaginatedResponse(); 26 | 27 | $this->assertEquals(self::DATA, $response->getData()); 28 | $this->assertEquals(self::OFFSET, $response->getOffset()); 29 | $this->assertEquals(self::LIMIT, $response->getLimit()); 30 | $this->assertEquals(self::TOTAL, $response->getTotal()); 31 | } 32 | 33 | public function testOffsetPaginatedResponseGettersSetters() 34 | { 35 | $response = new OffsetPaginatedResponse(null, 0, 0, 0); 36 | 37 | $response->setData(self::DATA); 38 | $response->setOffset(self::OFFSET); 39 | $response->setLimit(self::LIMIT); 40 | $response->setTotal(self::TOTAL); 41 | 42 | $this->assertEquals(self::DATA, $response->getData()); 43 | $this->assertEquals(self::OFFSET, $response->getOffset()); 44 | $this->assertEquals(self::LIMIT, $response->getLimit()); 45 | $this->assertEquals(self::TOTAL, $response->getTotal()); 46 | } 47 | 48 | public function testOffsetPaginatedResponseToArray() 49 | { 50 | $response = $this->createOffsetPaginatedResponse(); 51 | $data = $response->toArray(); 52 | 53 | $this->assertEquals(self::OFFSET, $data['offset']); 54 | $this->assertEquals(self::LIMIT, $data['limit']); 55 | $this->assertEquals(self::TOTAL, $data['total']); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Response/ResponseTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(json_encode($data), $response->getContent()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Response/ResponseTransformerTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('serialize'); 21 | 22 | $responseModel = new ResponseModel(); 23 | 24 | $responseModelFactory = m::mock('MediaMonks\RestApi\Model\ResponseModelFactory'); 25 | $responseModelFactory->shouldReceive('createFromContent')->andReturn($responseModel); 26 | 27 | $this->responseModelFactory = $responseModelFactory; 28 | 29 | return new ResponseTransformer($serializer, $responseModelFactory, $options); 30 | } 31 | 32 | public function testConstructSetsOptions() 33 | { 34 | $origin = 'postmsgorigin'; 35 | $subject = $this->getSubject(['post_message_origin' => $origin]); 36 | 37 | $this->assertEquals($origin, $subject->getPostMessageOrigin()); 38 | } 39 | 40 | public function testSetOptions() 41 | { 42 | $subject = $this->getSubject(); 43 | $origin = 'postmsgorigin'; 44 | 45 | $subject->setOptions(['post_message_origin' => $origin]); 46 | 47 | $this->assertEquals($origin, $subject->getPostMessageOrigin()); 48 | } 49 | 50 | public function testSetOptionsWithoutPostMessageOrigin() 51 | { 52 | $subject = $this->getSubject(); 53 | $origin = 'postmsgorigin'; 54 | 55 | $subject->setOptions(['someotherkey' => $origin]); 56 | 57 | $this->assertNull($subject->getPostMessageOrigin()); 58 | } 59 | 60 | public function testSetPostMessageOrigin() 61 | { 62 | $subject = $this->getSubject(); 63 | $origin = 'postmsgorigin'; 64 | 65 | $subject->setPostMessageOrigin($origin); 66 | 67 | $this->assertEquals($origin, $subject->getPostMessageOrigin()); 68 | } 69 | 70 | public function testTransformLateFalsePreconditions() 71 | { 72 | $subject = $this->getSubject(); 73 | 74 | $request = m::mock('Symfony\Component\HttpFoundation\Request'); 75 | $request->shouldReceive('getRequestFormat')->andReturn(Format::FORMAT_XML);; 76 | 77 | $response = m::mock('Symfony\Component\HttpFoundation\Response'); 78 | $response->shouldReceive('getStatusCode')->andReturn(Response::HTTP_OK); 79 | $response->shouldReceive('setCallback'); 80 | 81 | $subject->transformLate($request, $response); 82 | 83 | try { 84 | $response->shouldNotHaveReceived('setCallback'); 85 | $response->shouldNotHaveReceived('setContent'); 86 | $this->assertTrue(true); 87 | } catch (\Exception $e) { 88 | $this->assertTrue(false); 89 | } 90 | } 91 | 92 | public function testTransformLateWrapperPostMessage() 93 | { 94 | $subject = $this->getSubject(); 95 | 96 | $request = m::mock('Symfony\Component\HttpFoundation\Request'); 97 | $request->shouldReceive('getRequestFormat')->andReturn(Format::FORMAT_JSON); 98 | 99 | $request->query = m::mock('\Symfony\Component\HttpFoundation\ParameterBag'); 100 | $request->query->shouldReceive('has')->andReturn(true); 101 | $request->query->shouldReceive('get')->andReturn(ResponseTransformer::WRAPPER_POST_MESSAGE); 102 | 103 | $response = m::mock('MediaMonks\RestApi\Response\JsonResponse'); 104 | $response->shouldReceive('getStatusCode')->andReturn(Response::HTTP_OK); 105 | $response->shouldReceive('setContent')->andReturnSelf(); 106 | $response->shouldReceive('getContent')->andReturn('foo'); 107 | 108 | $response->headers = m::mock('\Symfony\Component\HttpFoundation\ResponseHeaderBag'); 109 | $response->headers->shouldReceive('set'); 110 | 111 | $subject->transformLate($request, $response); 112 | 113 | try { 114 | $response->shouldHaveReceived('setContent')->between(1, 1); 115 | $this->assertTrue(true); 116 | } catch (\Exception $e) { 117 | $this->assertTrue(false); 118 | } 119 | } 120 | 121 | public function testTransformLateWrapperCallback() 122 | { 123 | $subject = $this->getSubject(); 124 | 125 | $request = m::mock('Symfony\Component\HttpFoundation\Request'); 126 | $request->shouldReceive('getRequestFormat')->andReturn(Format::FORMAT_JSON); 127 | $request->query = m::mock('\Symfony\Component\HttpFoundation\ParameterBag'); 128 | $request->query->shouldReceive('has')->andReturn(true); 129 | $request->query->shouldReceive('get'); 130 | 131 | $response = m::mock('MediaMonks\RestApi\Response\JsonResponse'); 132 | $response->shouldReceive('getStatusCode')->andReturn(Response::HTTP_OK); 133 | $response->shouldReceive('setCallback'); 134 | 135 | $subject->transformLate($request, $response); 136 | 137 | try { 138 | $response->shouldHaveReceived('setCallback')->between(1, 1); 139 | $this->assertTrue(true); 140 | } catch (\Exception $e) { 141 | $this->assertTrue(false); 142 | } 143 | } 144 | 145 | public function testTransformEarlyWResponseModelHappyPath() 146 | { 147 | // Get SUT 148 | $subject = $this->getSubject(); 149 | 150 | $request = m::mock('Symfony\Component\HttpFoundation\Request'); 151 | $request->shouldReceive('getRequestFormat')->andReturn(Format::FORMAT_JSON); 152 | 153 | $request->headers = m::mock('Symfony\Component\HttpFoundation\HeaderBag'); 154 | $request->headers->shouldReceive('has')->andReturn(true); 155 | 156 | $request->query = m::mock('Symfony\Component\HttpFoundation\ParameterBag'); 157 | $request->query->shouldReceive('has')->andReturn(false); 158 | 159 | $responseModel = m::mock('MediaMonks\RestApi\Model\ResponseModel'); 160 | $responseModel->shouldReceive('getStatusCode')->andReturn(Response::HTTP_CONFLICT); 161 | $responseModel->shouldReceive('setReturnStatusCode'); 162 | $responseModel->shouldReceive('setReturnStackTrace'); 163 | $responseModel->shouldReceive('toArray'); 164 | 165 | $response = m::mock('Symfony\Component\HttpFoundation\Response'); 166 | $response->shouldReceive('getContent')->andReturn($responseModel); 167 | $response->shouldReceive('setStatusCode'); 168 | $response->shouldReceive('getStatusCode')->andReturn(Response::HTTP_CONFLICT); 169 | 170 | $response->headers = m::mock('Symfony\Component\HttpFoundation\ResponseHeaderBag'); 171 | $response->headers->shouldReceive('set'); 172 | 173 | // Perform operation 174 | $actualResponse = $subject->transformEarly($request, $response); 175 | 176 | // Verify post-conditions 177 | $this->assertNoException(function () use ($response) { 178 | $response->shouldHaveReceived('setStatusCode'); 179 | $this->assertEquals(Response::HTTP_CONFLICT, $response->getStatusCode()); 180 | }); 181 | 182 | $this->assertInstanceOf('Symfony\Component\HttpFoundation\JsonResponse', $actualResponse); 183 | } 184 | 185 | public function testTransformEarlyForceHttpOk() 186 | { 187 | $subject = $this->getSubject(); 188 | 189 | $request = new Request(); 190 | $response = new Response('foo', Response::HTTP_NO_CONTENT); 191 | $response->headers->set('Content-Type', 'text/html'); 192 | 193 | $subject->transformLate($request, $response); 194 | 195 | $this->assertEmpty($response->getContent()); 196 | $this->assertFalse($response->headers->has('Content-Type')); 197 | } 198 | 199 | public function testForceEmptyResponseOnHttpNoContent() 200 | { 201 | // Get SUT 202 | $subject = $this->getSubject(); 203 | 204 | // Prepare/mock pre-conditions 205 | $request = m::mock('Symfony\Component\HttpFoundation\Request'); 206 | $request->shouldReceive('getRequestFormat')->andReturn(Format::FORMAT_JSON); 207 | 208 | $request->headers = m::mock('Symfony\Component\HttpFoundation\HeaderBag'); 209 | $request->headers->shouldReceive('has')->andReturn(true); 210 | 211 | $request->query = m::mock('Symfony\Component\HttpFoundation\ParameterBag'); 212 | $request->query->shouldReceive('has')->andReturn(false); 213 | 214 | $responseModel = m::mock('MediaMonks\RestApi\Model\ResponseModel'); 215 | $responseModel->shouldReceive('getStatusCode')->andReturn(Response::HTTP_NO_CONTENT); 216 | $responseModel->shouldReceive('setReturnStatusCode'); 217 | $responseModel->shouldReceive('setReturnStackTrace'); 218 | $responseModel->shouldReceive('toArray'); 219 | 220 | $response = m::mock('Symfony\Component\HttpFoundation\Response'); 221 | $response->shouldReceive('getContent')->andReturn($responseModel); 222 | $response->shouldReceive('setStatusCode'); 223 | $response->shouldReceive('getStatusCode')->andReturn(200); 224 | 225 | $response->headers = m::mock('Symfony\Component\HttpFoundation\ResponseHeaderBag'); 226 | $response->headers->shouldReceive('set'); 227 | 228 | // Perform operation 229 | $subject->transformEarly($request, $response); 230 | 231 | // Verify post-conditions 232 | $this->assertNoException(function () use ($response) { 233 | $response->shouldHaveReceived('setStatusCode')->with(Response::HTTP_OK); 234 | }); 235 | } 236 | 237 | public function testTransformEarlySerializeXml() 238 | { 239 | // Get SUT 240 | $subject = $this->getSubject(); 241 | 242 | // Prepare/mock pre-conditions 243 | $request = m::mock('Symfony\Component\HttpFoundation\Request'); 244 | $request->shouldReceive('getRequestFormat')->andReturn(Format::FORMAT_XML); 245 | 246 | $request->headers = m::mock('Symfony\Component\HttpFoundation\HeaderBag'); 247 | $request->headers->shouldReceive('has')->andReturn(true); 248 | 249 | $request->query = m::mock('Symfony\Component\HttpFoundation\ParameterBag'); 250 | $request->query->shouldReceive('has')->andReturn(false); 251 | 252 | $responseModel = m::mock('MediaMonks\RestApi\Model\ResponseModel'); 253 | $responseModel->shouldReceive('getStatusCode')->andReturn(Response::HTTP_CONFLICT); 254 | $responseModel->shouldReceive('setReturnStatusCode'); 255 | $responseModel->shouldReceive('setReturnStackTrace'); 256 | $responseModel->shouldReceive('toArray'); 257 | 258 | $response = m::mock('Symfony\Component\HttpFoundation\Response'); 259 | $response->shouldReceive('getContent')->andReturn($responseModel); 260 | $response->shouldReceive('setStatusCode'); 261 | $response->shouldReceive('getStatusCode')->andReturn(200); 262 | $response->shouldReceive('setContent'); 263 | 264 | $response->headers = m::mock('Symfony\Component\HttpFoundation\ResponseHeaderBag'); 265 | $response->headers->shouldReceive('set'); 266 | 267 | // Perform operation 268 | $subject->transformEarly($request, $response); 269 | 270 | // Verify post-conditions 271 | $this->assertNoException(function () use ($response) { 272 | $response->shouldHaveReceived('setStatusCode')->with(Response::HTTP_OK); 273 | $response->shouldHaveReceived('setContent'); 274 | }); 275 | } 276 | 277 | public function testTransformEarlyWOResponseModel() 278 | { 279 | // Get SUT 280 | $subject = $this->getSubject(); 281 | 282 | // Prepare/mock pre-conditions 283 | $content = 'some content'; 284 | 285 | $request = m::mock('Symfony\Component\HttpFoundation\Request'); 286 | $request->shouldReceive('getRequestFormat')->andReturn(Format::FORMAT_XML); 287 | 288 | $request->headers = m::mock('Symfony\Component\HttpFoundation\HeaderBag'); 289 | $request->headers->shouldReceive('has')->andReturn(true); 290 | 291 | $request->query = m::mock('Symfony\Component\HttpFoundation\ParameterBag'); 292 | $request->query->shouldReceive('has')->andReturn(false); 293 | 294 | $response = m::mock('Symfony\Component\HttpFoundation\Response'); 295 | $response->shouldReceive('getContent')->andReturn($content); 296 | $response->shouldReceive('setStatusCode'); 297 | $response->shouldReceive('getStatusCode')->andReturn(200); 298 | $response->shouldReceive('setContent'); 299 | 300 | $response->headers = m::mock('Symfony\Component\HttpFoundation\ResponseHeaderBag'); 301 | $response->headers->shouldReceive('set'); 302 | 303 | $responseModel = m::mock('MediaMonks\RestApi\Model\ResponseModel'); 304 | $responseModel->shouldReceive('getStatusCode')->andReturn(Response::HTTP_CONFLICT); 305 | $responseModel->shouldReceive('setReturnStatusCode'); 306 | $responseModel->shouldReceive('setReturnStackTrace'); 307 | $responseModel->shouldReceive('toArray'); 308 | 309 | // Perform operation 310 | $subject->transformEarly($request, $response); 311 | 312 | $factory = $this->responseModelFactory; 313 | 314 | // Verify post-conditions 315 | $this->assertNoException(function () use ($factory, $content) { 316 | $factory->shouldHaveReceived('createFromContent'); 317 | }); 318 | } 319 | 320 | public function testDebugGetSet() 321 | { 322 | $subject = $this->getSubject(); 323 | 324 | $this->assertFalse($subject->isDebug()); 325 | $subject->setDebug(true); 326 | $this->assertTrue($subject->isDebug()); 327 | 328 | $subject = $this->getSubject(['debug' => true]); 329 | $this->assertTrue($subject->isDebug()); 330 | } 331 | 332 | public function testCreateResponseFromContent() 333 | { 334 | $subject = $this->getSubject(); 335 | $responseModel = $subject->createResponseFromContent('foo'); 336 | 337 | $this->assertInstanceOf('MediaMonks\RestApi\Response\Response', $responseModel); 338 | } 339 | 340 | protected function assertNoException($callback) 341 | { 342 | try { 343 | $callback(); 344 | $this->assertTrue(true); 345 | } catch (\Exception $e) { 346 | $this->fail('Exception thrown when none was expected. Exception message: ' . $e->getMessage()); 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /tests/Serializer/ChainSerializerTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getSupportedFormats')->andReturn(['json']); 17 | $jsonSerializer->shouldReceive('getDefaultFormat')->andReturn('json'); 18 | 19 | $serializer->addSerializer($jsonSerializer); 20 | $this->assertEquals(['json'], $serializer->getSupportedFormats()); 21 | $this->assertEquals('json', $serializer->getDefaultFormat()); 22 | $this->assertTrue($serializer->supportsFormat('json')); 23 | $this->assertFalse($serializer->supportsFormat('xml')); 24 | 25 | $msgpackSerializer = m::mock('MediaMonks\RestApi\Serializer\MsgpackSerializer'); 26 | $msgpackSerializer->shouldReceive('getSupportedFormats')->andReturn(['msgpack']); 27 | $msgpackSerializer->shouldReceive('getDefaultFormat')->andReturn('msgpack'); 28 | 29 | $serializer->addSerializer($msgpackSerializer); 30 | $this->assertEquals(['json', 'msgpack'], $serializer->getSupportedFormats()); 31 | $this->assertEquals('json', $serializer->getDefaultFormat()); 32 | $this->assertTrue($serializer->supportsFormat('json')); 33 | $this->assertTrue($serializer->supportsFormat('msgpack')); 34 | } 35 | 36 | public function test_supported_formats_without_serializer() 37 | { 38 | $this->expectException('MediaMonks\RestApi\Exception\SerializerException'); 39 | 40 | $serializer = new ChainSerializer(); 41 | $serializer->getSupportedFormats(); 42 | } 43 | 44 | public function test_default_format_without_serializer() 45 | { 46 | $this->expectException('MediaMonks\RestApi\Exception\SerializerException'); 47 | 48 | $serializer = new ChainSerializer(); 49 | $serializer->getDefaultFormat(); 50 | } 51 | 52 | public function test_serialize_json() 53 | { 54 | $serializer = new ChainSerializer(); 55 | 56 | $jsonSerializer = m::mock('MediaMonks\RestApi\Serializer\JsonSerializer'); 57 | $jsonSerializer->shouldReceive('getSupportedFormats')->andReturn(['json']); 58 | $jsonSerializer->shouldReceive('getDefaultFormat')->andReturn('json'); 59 | $jsonSerializer->shouldReceive('serialize')->andReturn('json_output'); 60 | $jsonSerializer->shouldReceive('supportsFormat')->withArgs(['json'])->andReturn(true); 61 | $jsonSerializer->shouldReceive('supportsFormat')->withArgs(['msgpack'])->andReturn(false); 62 | 63 | $serializer->addSerializer($jsonSerializer); 64 | 65 | $msgpackSerializer = m::mock('MediaMonks\RestApi\Serializer\MsgpackSerializer'); 66 | $msgpackSerializer->shouldReceive('getSupportedFormats')->andReturn(['msgpack']); 67 | $msgpackSerializer->shouldReceive('getDefaultFormat')->andReturn('msgpack'); 68 | $msgpackSerializer->shouldReceive('serialize')->andReturn('msgpack_output'); 69 | $msgpackSerializer->shouldReceive('supportsFormat')->withArgs(['json'])->andReturn(false); 70 | $msgpackSerializer->shouldReceive('supportsFormat')->withArgs(['msgpack'])->andReturn(true); 71 | 72 | $serializer->addSerializer($msgpackSerializer); 73 | $this->assertEquals('json_output', $serializer->serialize('foo', 'json')); 74 | $this->assertEquals('msgpack_output', $serializer->serialize('foo', 'msgpack')); 75 | } 76 | 77 | public function test_serialize_without_serializer() 78 | { 79 | $this->expectException('MediaMonks\RestApi\Exception\SerializerException'); 80 | 81 | $serializer = new ChainSerializer(); 82 | $serializer->serialize('foo', 'json'); 83 | } 84 | 85 | public function test_serialize_without_serializer_with_unsupported_format() 86 | { 87 | $this->expectException('MediaMonks\RestApi\Exception\SerializerException'); 88 | 89 | $serializer = new ChainSerializer(); 90 | 91 | $jsonSerializer = m::mock('MediaMonks\RestApi\Serializer\JsonSerializer'); 92 | $jsonSerializer->shouldReceive('getSupportedFormats')->andReturn([]); 93 | $jsonSerializer->shouldReceive('supportsFormat')->andReturn(false); 94 | $serializer->addSerializer($jsonSerializer); 95 | 96 | $serializer->serialize('foo', 'xml'); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Serializer/JMSSerializerTest.php: -------------------------------------------------------------------------------- 1 | assertIsArray($serializer->getSupportedFormats()); 17 | $this->assertEquals(['json', 'xml'], $serializer->getSupportedFormats()); 18 | $this->assertIsString($serializer->getDefaultFormat()); 19 | $this->assertEquals('json', $serializer->getDefaultFormat()); 20 | } 21 | 22 | public function test_supports() 23 | { 24 | $jmsSerializer = m::mock('JMS\Serializer\Serializer'); 25 | 26 | $serializer = new JMSSerializer($jmsSerializer); 27 | $this->assertTrue($serializer->supportsFormat('json')); 28 | $this->assertTrue($serializer->supportsFormat('xml')); 29 | $this->assertFalse($serializer->supportsFormat('msgpack')); 30 | } 31 | 32 | public function test_serialize() 33 | { 34 | $jmsSerializer = m::mock('JMS\Serializer\Serializer'); 35 | $jmsSerializer->shouldReceive('serialize')->once()->withArgs(['foo', 'json', null])->andReturn('"foo"'); 36 | 37 | $serializer = new JMSSerializer($jmsSerializer); 38 | $output = $serializer->serialize('foo', 'json'); 39 | $this->assertEquals('"foo"', $output); 40 | } 41 | 42 | public function test_serialize_xml() 43 | { 44 | $jmsSerializer = m::mock('JMS\Serializer\Serializer'); 45 | $jmsSerializer->shouldReceive('serialize')->once()->withArgs(['foo', 'xml', null])->andReturn(''); 46 | 47 | $serializer = new JMSSerializer($jmsSerializer); 48 | $output = $serializer->serialize('foo', 'xml'); 49 | $this->assertEquals('', $output); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Serializer/JsonSerializerTest.php: -------------------------------------------------------------------------------- 1 | assertIsArray($serializer->getSupportedFormats()); 14 | $this->assertEquals(['json'], $serializer->getSupportedFormats()); 15 | $this->assertIsString($serializer->getDefaultFormat()); 16 | $this->assertEquals('json', $serializer->getDefaultFormat()); 17 | } 18 | 19 | public function test_supports() 20 | { 21 | $serializer = new JsonSerializer(); 22 | $this->assertTrue($serializer->supportsFormat('json')); 23 | $this->assertFalse($serializer->supportsFormat('xml')); 24 | $this->assertFalse($serializer->supportsFormat('msgpack')); 25 | } 26 | 27 | public function test_serialize() 28 | { 29 | $serializer = new JsonSerializer(); 30 | $output = $serializer->serialize('foo', 'json'); 31 | $this->assertEquals('"foo"', $output); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Serializer/MsgpackSerializerTest.php: -------------------------------------------------------------------------------- 1 | assertIsArray($serializer->getSupportedFormats()); 26 | $this->assertEquals(['msgpack'], $serializer->getSupportedFormats()); 27 | $this->assertIsString($serializer->getDefaultFormat()); 28 | $this->assertEquals('msgpack', $serializer->getDefaultFormat()); 29 | } 30 | 31 | public function test_supports() 32 | { 33 | $serializer = new MsgpackSerializer(); 34 | $this->assertFalse($serializer->supportsFormat('json')); 35 | $this->assertFalse($serializer->supportsFormat('xml')); 36 | $this->assertTrue($serializer->supportsFormat('msgpack')); 37 | } 38 | 39 | public function test_serialize() 40 | { 41 | $serializer = new MsgpackSerializer(); 42 | $output = $serializer->serialize([0 => 1], 'msgpack'); 43 | $this->assertEquals("\x91\x01", $output); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /tests/Util/StringUtilTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('not_found', StringUtil::classToSnakeCase(new NotFoundHttpException, 'HttpException')); 16 | $this->assertEquals('bad_request', StringUtil::classToSnakeCase(new BadRequestHttpException, 'HttpException')); 17 | $this->assertEquals('not_blank', StringUtil::classToSnakeCase(new Constraint\NotBlank)); 18 | $this->assertEquals('email', StringUtil::classToSnakeCase(new Constraint\Email)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |