├── .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 | [](https://github.com/mediamonks/php-rest-api/actions?query=workflow%3ACI)
2 | [](https://codecov.io/gh/mediamonks/php-rest-api)
3 | [](https://packagist.org/packages/mediamonks/rest-api)
4 | [](https://packagist.org/packages/mediamonks/rest-api)
5 | [](https://packagist.org/packages/mediamonks/rest-api)
6 | [](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 |