├── .gitignore ├── LICENSE ├── README.md ├── composer.json └── src ├── DependencyInjection ├── Configuration.php └── NilPortuguesSymfonyJsonApiExtension.php ├── NilPortuguesSymfonyJsonApiBundle.php ├── Resources └── config │ └── services.yml └── Serializer ├── JsonApiResponseTrait.php └── JsonApiSerializer.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | .env 3 | composer.lock 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nil Portugués Calderó 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Symfony JSON-API Transformer Bundle 2 | ========================================= 3 | For Symfony 2 and Symfony 3 4 | 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nilportugues/symfony2-jsonapi-transformer/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/nilportugues/symfony2-jsonapi-transformer/?branch=master) 6 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/3269f12e-a707-462a-bef5-22e5ed522e8e/mini.png?)](https://insight.sensiolabs.com/projects/3269f12e-a707-462a-bef5-22e5ed522e8e) 7 | [![Latest Stable Version](https://poser.pugx.org/nilportugues/jsonapi-bundle/v/stable)](https://packagist.org/packages/nilportugues/jsonapi-bundle) 8 | [![Total Downloads](https://poser.pugx.org/nilportugues/jsonapi-bundle/downloads)](https://packagist.org/packages/nilportugues/jsonapi-bundle) 9 | [![License](https://poser.pugx.org/nilportugues/jsonapi-bundle/license)](https://packagist.org/packages/nilportugues/jsonapi-bundle) 10 | [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://paypal.me/nilportugues) 11 | 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | - [Creating the mappings](#creating-the-mappings) 15 | - [Outputing API Responses](#outputing-api-responses) 16 | - [JSON API Requests](#request-objects) 17 | - [JSON API Responses](#response-objects-jsonapiresponsetrait) 18 | - [Integration with NelmioApiDocBundleBundle](#integration-with-nelmioapidocbundlebundle) 19 | - [Quality](#quality) 20 | - [Contribute](#contribute) 21 | - [Support](#support) 22 | - [Authors](#authors) 23 | - [License](#license) 24 | 25 | 26 | 27 | ## Installation 28 | 29 | **Step 1: Download the Bundle** 30 | 31 | Open a command console, enter your project directory and execute the 32 | following command to download the latest stable version of this bundle: 33 | 34 | ```bash 35 | $ composer require nilportugues/jsonapi-bundle 36 | ``` 37 | 38 | 39 | **Step 2: Enable the Bundle** 40 | 41 | Then, enable the bundle by adding it to the list of registered bundles 42 | in the `app/AppKernel.php` file of your project: 43 | 44 | ```php 45 | (new DateTime('2015/07/18 12:13:00'))->format('c'), 106 | 'accepted_at' => (new DateTime('2015/07/19 00:00:00'))->format('c'), 107 | ] 108 | ), 109 | ] 110 | ); 111 | ``` 112 | 113 | And the series of mapping files required: 114 | 115 | ```yml 116 | # app/config/serializer/acme_domain_dummy_post.yml 117 | 118 | mapping: 119 | class: Acme\Domain\Dummy\Post 120 | alias: Message 121 | aliased_properties: 122 | author: author 123 | title: headline 124 | content: body 125 | hide_properties: [] 126 | id_properties: 127 | - postId 128 | urls: 129 | self: get_post ## @Route name 130 | comments: get_post_comments ## @Route name 131 | relationships: 132 | author: 133 | related: get_post_author ## @Route name 134 | self: get_post_author_relationship ## @Route name 135 | ``` 136 | 137 | ```yml 138 | # app/config/serializer/acme_domain_dummy_value_object_post_id.yml 139 | 140 | mapping: 141 | class: Acme\Domain\Dummy\ValueObject\PostId 142 | aliased_properties: [] 143 | hide_properties: [] 144 | id_properties: 145 | - postId 146 | urls: 147 | self: get_post ## @Route name 148 | relationships: 149 | comment: 150 | self: get_post_comments_relationship ## @Route name 151 | ``` 152 | 153 | 154 | ```yml 155 | # app/config/serializer/acme_domain_dummy_comment.yml 156 | 157 | mapping: 158 | class: Acme\Domain\Dummy\Comment 159 | aliased_properties: [] 160 | hide_properties: [] 161 | id_properties: 162 | - commentId 163 | urls: 164 | self: get_comment ## @Route name 165 | relationships: 166 | post: 167 | self: get_post_comments_relationship ## @Route name 168 | ``` 169 | 170 | ```yml 171 | # app/config/serializer/acme_domain_dummy_value_object_comment_id.yml 172 | 173 | mapping: 174 | class: Acme\Domain\Dummy\ValueObject\CommentId 175 | aliased_properties: [] 176 | hide_properties: [] 177 | id_properties: 178 | - commentId 179 | urls: 180 | self: get_comment ## @Route name 181 | relationships: 182 | post: 183 | self: get_post_comments_relationship ## @Route name 184 | ``` 185 | 186 | 187 | ```yml 188 | # app/config/serializer/acme_domain_dummy_user.yml 189 | 190 | mapping: 191 | class: Acme\Domain\Dummy\User 192 | aliased_properties: [] 193 | hide_properties: [] 194 | id_properties: 195 | - userId 196 | urls: 197 | self: get_user 198 | friends: get_user_friends ## @Route name 199 | comments: get_user_comments ## @Route name 200 | ``` 201 | 202 | 203 | ```yml 204 | # app/config/serializer/acme_domain_dummy_value_object_user_id.yml 205 | 206 | mapping: 207 | class: Acme\Domain\Dummy\ValueObject\UserId 208 | aliased_properties: [] 209 | hide_properties: [] 210 | id_properties: 211 | - userId 212 | urls: 213 | self: get_user ## @Route name 214 | friends: get_user_friends ## @Route name 215 | comments: get_user_comments ## @Route name 216 | ``` 217 | 218 | 219 | ## Outputing API Responses 220 | 221 | It is really easy, just get an instance of the `JsonApiSerializer` from the **Service Container** and pass the object to its `serialize()` method. Output will be valid JSON-API. 222 | 223 | Here's an example of a `Post` object being fetched from a Doctrine repository. 224 | 225 | Finally, a helper trait, `JsonApiResponseTrait` is provided to write fully compilant responses wrapping the PSR-7 Response objects provided by the original JSON API Transformer library. 226 | 227 | ```php 228 | get('doctrine.post_repository')->find($postId); 247 | 248 | $serializer = $this->get('nil_portugues.serializer.json_api_serializer'); 249 | 250 | /** @var \NilPortugues\Api\JsonApi\JsonApiTransformer $transformer */ 251 | $transformer = $serializer->getTransformer(); 252 | $transformer->setSelfUrl($this->generateUrl('get_post', ['postId' => $postId], true)); 253 | $transformer->setNextUrl($this->generateUrl('get_post', ['postId' => $postId+1], true)); 254 | 255 | return $this->response($serializer->serialize($post)); 256 | } 257 | } 258 | ``` 259 | 260 | 261 | **Output:** 262 | 263 | ``` 264 | HTTP/1.1 200 OK 265 | Cache-Control: private, max-age=0, must-revalidate 266 | Content-type: application/vnd.api+json 267 | ``` 268 | 269 | ```json 270 | { 271 | "data": { 272 | "type": "message", 273 | "id": "9", 274 | "attributes": { 275 | "headline": "Hello World", 276 | "body": "Your first post" 277 | }, 278 | "links": { 279 | "self": { 280 | "href": "http://example.com/posts/9" 281 | }, 282 | "comments": { 283 | "href": "http://example.com/posts/9/comments" 284 | } 285 | }, 286 | "relationships": { 287 | "author": { 288 | "links": { 289 | "self": { 290 | "href": "http://example.com/posts/9/relationships/author" 291 | }, 292 | "related": { 293 | "href": "http://example.com/posts/9/author" 294 | } 295 | }, 296 | "data": { 297 | "type": "user", 298 | "id": "1" 299 | } 300 | } 301 | } 302 | }, 303 | "included": [ 304 | { 305 | "type": "user", 306 | "id": "1", 307 | "attributes": { 308 | "name": "Post Author" 309 | }, 310 | "links": { 311 | "self": { 312 | "href": "http://example.com/users/1" 313 | }, 314 | "friends": { 315 | "href": "http://example.com/users/1/friends" 316 | }, 317 | "comments": { 318 | "href": "http://example.com/users/1/comments" 319 | } 320 | } 321 | }, 322 | { 323 | "type": "user", 324 | "id": "2", 325 | "attributes": { 326 | "name": "Barristan Selmy" 327 | }, 328 | "links": { 329 | "self": { 330 | "href": "http://example.com/users/2" 331 | }, 332 | "friends": { 333 | "href": "http://example.com/users/2/friends" 334 | }, 335 | "comments": { 336 | "href": "http://example.com/users/2/comments" 337 | } 338 | } 339 | }, 340 | { 341 | "type": "comment", 342 | "id": "1000", 343 | "attributes": { 344 | "dates": { 345 | "created_at": "2015-08-13T21:11:07+02:00", 346 | "accepted_at": "2015-08-13T21:46:07+02:00" 347 | }, 348 | "comment": "Have no fear, sers, your king is safe." 349 | }, 350 | "relationships": { 351 | "user": { 352 | "data": { 353 | "type": "user", 354 | "id": "2" 355 | } 356 | } 357 | }, 358 | "links": { 359 | "self": { 360 | "href": "http://example.com/comments/1000" 361 | } 362 | } 363 | } 364 | ], 365 | "links": { 366 | "self": { 367 | "href": "http://example.com/posts/9" 368 | }, 369 | "next": { 370 | "href": "http://example.com/posts/10" 371 | } 372 | }, 373 | "jsonapi": { 374 | "version": "1.0" 375 | } 376 | } 377 | ``` 378 | 379 | #### Request objects 380 | 381 | JSON API comes with a helper Request class, `NilPortugues\Api\JsonApi\Http\Request\Request(ServerRequestInterface $request)`, implementing the PSR-7 Request Interface. Using this request object will provide you access to all the interactions expected in a JSON API: 382 | 383 | ##### JSON API Query Parameters: 384 | 385 | - **&fields[resource]=field1,field2** will only show the specified fields for a given resource. 386 | - **&include=resource** show the relationship for a given resource. 387 | - **&include=resource.resource2** show the relationship field for those depending on resource2. 388 | - **&sort=field1,-field2** sort by field2 as DESC and field1 as ASC. 389 | - **&sort=-field1,field2** sort by field1 as DESC and field2 as ASC. 390 | - **&page[number]** will return the current page elements in a *page-based* pagination strategy. 391 | - **&page[size]** will return the total amout of elements in a *page-based* pagination strategy. 392 | - **&page[limit]** will return the limit in a *offset-based* pagination strategy. 393 | - **&page[offset]** will return the offset value in a *offset-based* pagination strategy. 394 | - **&page[cursor]** will return the cursor value in a *cursor-based* pagination strategy. 395 | - **&filter** will return data passed in the filter param. 396 | 397 | 398 | ##### NilPortugues\Api\JsonApi\Http\Request\Request 399 | 400 | Given the query parameters listed above, Request implements helper methods that parse and return data already prepared. 401 | 402 | ```php 403 | namespace \NilPortugues\Api\JsonApi\Http\Request; 404 | 405 | class Request 406 | { 407 | public function __construct(ServerRequestInterface $request = null) { ... } 408 | public function getIncludedRelationships() { ... } 409 | public function getSort() { ... } 410 | public function getPage() { ... } 411 | public function getFilters() { ... } 412 | public function getFields() { ... } 413 | } 414 | ``` 415 | 416 | #### Response objects (JsonApiResponseTrait) 417 | 418 | The following `JsonApiResponseTrait` methods are provided to return the right headers and HTTP status codes are available: 419 | 420 | ```php 421 | private function errorResponse($json); 422 | private function resourceCreatedResponse($json); 423 | private function resourceDeletedResponse($json); 424 | private function resourceNotFoundResponse($json); 425 | private function resourcePatchErrorResponse($json); 426 | private function resourcePostErrorResponse($json); 427 | private function resourceProcessingResponse($json); 428 | private function resourceUpdatedResponse($json); 429 | private function response($json); 430 | private function unsupportedActionResponse($json); 431 | ``` 432 | ## Integration with NelmioApiDocBundleBundle 433 | 434 | The [NelmioApiDocBundle](https://github.com/nelmio/NelmioApiDocBundle/blob/master/Resources/doc/index.md) is a very well known bundle used to document APIs. Integration with the current bundle is terrible easy. 435 | 436 | Here's an example following the `PostContoller::getPostAction()` provided before: 437 | 438 | ```php 439 | get('doctrine.post_repository')->find($postId); 466 | 467 | $serializer = $this->get('nil_portugues.serializer.json_api_serializer'); 468 | 469 | /** @var \NilPortugues\Api\JsonApi\JsonApiTransformer $transformer */ 470 | $transformer = $serializer->getTransformer(); 471 | $transformer->setSelfUrl($this->generateUrl('get_post', ['postId' => $postId], true)); 472 | $transformer->setNextUrl($this->generateUrl('get_post', ['postId' => $postId+1], true)); 473 | 474 | return $this->response($serializer->serialize($post)); 475 | } 476 | } 477 | ``` 478 | 479 | And the recommended configuration to be added in `app/config/config.yml` 480 | 481 | ```yml 482 | #app/config/config.yml 483 | 484 | nelmio_api_doc: 485 | sandbox: 486 | authentication: 487 | name: access_token 488 | delivery: http 489 | type: basic 490 | custom_endpoint: false 491 | enabled: true 492 | endpoint: ~ 493 | accept_type: ~ 494 | body_format: 495 | formats: [] 496 | default_format: form 497 | request_format: 498 | formats: 499 | json: application/vnd.api+json 500 | method: accept_header 501 | default_format: json 502 | entity_to_choice: false 503 | ``` 504 | 505 | 506 | ## Quality 507 | 508 | To run the PHPUnit tests at the command line, go to the tests directory and issue phpunit. 509 | 510 | This library attempts to comply with [PSR-1](http://www.php-fig.org/psr/psr-1/), [PSR-2](http://www.php-fig.org/psr/psr-2/), [PSR-4](http://www.php-fig.org/psr/psr-4/) and [PSR-7](http://www.php-fig.org/psr/psr-7/). 511 | 512 | If you notice compliance oversights, please send a patch via [Pull Request](https://github.com/nilportugues/Symfony-jsonapi-transformer/pulls). 513 | 514 | 515 | ## Contribute 516 | 517 | Contributions to the package are always welcome! 518 | 519 | * Report any bugs or issues you find on the [issue tracker](https://github.com/nilportugues/Symfony-jsonapi-transformer/issues/new). 520 | * You can grab the source code at the package's [Git repository](https://github.com/nilportugues/Symfony-jsonapi-transformer). 521 | 522 | 523 | ## Support 524 | 525 | Get in touch with me using one of the following means: 526 | 527 | - Emailing me at 528 | - Opening an [Issue](https://github.com/nilportugues/Symfony-jsonapi-transformer/issues/new) 529 | - Using Gitter: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/nilportugues/Symfony-jsonapi-transformer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 530 | 531 | 532 | ## Authors 533 | 534 | * [Nil Portugués Calderó](http://nilportugues.com) 535 | * [The Community Contributors](https://github.com/nilportugues/Symfony-jsonapi-transformer/graphs/contributors) 536 | 537 | 538 | ## License 539 | The code base is licensed under the [MIT license](LICENSE). 540 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nilportugues/jsonapi-bundle", 3 | "homepage": "http://nilportugues.com", 4 | "type": "library", 5 | "description": "Symfony 2 & 3 JSON API Transformer Package", 6 | "keywords": ["sf2", "symfony", "symfony2", "symfony3", "lumen", "json", "api", "jsonapi", "serializer", "transformer", "psr7", "response"], 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Nil Portugués Calderó", 11 | "email": "contact@nilportugues.com", 12 | "role": "Project Lead Developer" 13 | } 14 | ], 15 | "autoload": { 16 | "psr-4": { 17 | "NilPortugues\\Symfony\\JsonApiBundle\\": "src/" 18 | } 19 | }, 20 | "require": { 21 | "php": ">=5.5.0", 22 | "nilportugues/json-api": "^2.4" 23 | }, 24 | "require-dev": { 25 | "symfony/symfony": "^2.0|^3.0", 26 | "friendsofphp/php-cs-fixer": "^1.9", 27 | "nilportugues/php_backslasher": "^0.2" 28 | }, 29 | "config": { 30 | "preferred-install": "dist" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('nilportugues_json_api') 25 | ->children() 26 | ->arrayNode('mappings') 27 | ->prototype('scalar') 28 | ->isRequired() 29 | ->cannotBeEmpty() 30 | ->end() 31 | ->end() 32 | ->scalarNode('attributes_case') 33 | ->defaultValue('snake_case') 34 | ->validate() 35 | ->ifNotInArray(['snake_case', 'keep_case']) // @TODO: implement forcing camelCase and hypen-case. 36 | ->thenInvalid('Invalid case setting %s, valid values are: snake_case, keep_case') 37 | ->end() 38 | ->end(); 39 | 40 | return $treeBuilder; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/DependencyInjection/NilPortuguesSymfonyJsonApiExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 27 | 28 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 29 | $loader->load('services.yml'); 30 | $this->setMappings($container, $config); 31 | $this->setAttributesCase($container, $config); 32 | } 33 | 34 | /** 35 | * @param ContainerBuilder $container 36 | * @param $config 37 | */ 38 | private function setMappings(ContainerBuilder $container, $config) 39 | { 40 | $definition = new Definition(); 41 | $definition->setClass('NilPortugues\Api\Mapping\Mapper'); 42 | $args = $this->resolveMappings($container, $config['mappings']); 43 | $definition->setArguments($args); 44 | $definition->setLazy(true); 45 | 46 | $container->setDefinition('nil_portugues.api.mapping.mapper', $definition); 47 | } 48 | 49 | private function resolveMappings(ContainerBuilder $container, $mappings) 50 | { 51 | $loadedMappings = []; 52 | 53 | foreach ($mappings as $mapping) { 54 | if (0 === strpos($mapping, '@')) { 55 | $name = substr($mapping, 1, strpos($mapping, '/') - 1); 56 | 57 | $dir = $this->resolveBundle($container, $name); 58 | $mapping = str_replace('@'.$name, $dir, $mapping); 59 | } 60 | 61 | if (true === \file_exists($mapping)) { 62 | $finder = new Finder(); 63 | $finder->files()->in($mapping); 64 | foreach ($finder as $file) { 65 | /* @var \Symfony\Component\Finder\SplFileInfo $file */ 66 | $mapping = \file_get_contents($file->getPathname()); 67 | $mapping = Yaml::parse($mapping); 68 | $loadedMappings[] = $mapping['mapping']; 69 | } 70 | } 71 | } 72 | 73 | return [$loadedMappings]; 74 | } 75 | 76 | /** 77 | * @param ContainerBuilder $container 78 | * @param $config 79 | */ 80 | private function setAttributesCase(ContainerBuilder $container, $config) 81 | { 82 | $container->setParameter('nil_portugues.api.attributes_case', $config['attributes_case']); 83 | } 84 | 85 | private function resolveBundle(ContainerBuilder $container, $name) 86 | { 87 | $bundles = $container->getParameter('kernel.bundles'); 88 | 89 | if (!isset($bundles[$name])) { 90 | return; 91 | } 92 | 93 | $class = $bundles[$name]; 94 | $refClass = new \ReflectionClass($class); 95 | 96 | return dirname($refClass->getFileName()); 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function getAlias() 103 | { 104 | return 'nilportugues_json_api'; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/NilPortuguesSymfonyJsonApiBundle.php: -------------------------------------------------------------------------------- 1 | createResponse(new BadRequest(new ErrorBag([$error]))); 41 | } 42 | 43 | /** 44 | * @param string $json 45 | * 46 | * @return \Symfony\Component\HttpFoundation\Response 47 | */ 48 | protected function resourceCreatedResponse($json) 49 | { 50 | return $this->createResponse(new ResourceCreated($json)); 51 | } 52 | 53 | /** 54 | * @param string $json 55 | * 56 | * @return \Symfony\Component\HttpFoundation\Response 57 | */ 58 | protected function resourceDeletedResponse($json) 59 | { 60 | return $this->createResponse(new ResourceDeleted($json)); 61 | } 62 | 63 | /** 64 | * @param string $json 65 | * 66 | * @return \Symfony\Component\HttpFoundation\Response 67 | */ 68 | protected function resourceNotFoundResponse($json) 69 | { 70 | $error = new Error('Resource not Found', json_decode($json)); 71 | 72 | return $this->createResponse(new ResourceNotFound(new ErrorBag([$error]))); 73 | } 74 | 75 | /** 76 | * @param string $json 77 | * 78 | * @return \Symfony\Component\HttpFoundation\Response 79 | */ 80 | protected function resourcePatchErrorResponse($json) 81 | { 82 | $error = new Error('Unprocessable Entity', json_decode($json)); 83 | 84 | return $this->createResponse(new UnprocessableEntity([$error])); 85 | } 86 | 87 | /** 88 | * @param string $json 89 | * 90 | * @return \Symfony\Component\HttpFoundation\Response 91 | */ 92 | protected function resourcePostErrorResponse($json) 93 | { 94 | $error = new Error('Unprocessable Entity', json_decode($json)); 95 | 96 | return $this->createResponse(new UnprocessableEntity([$error])); 97 | } 98 | 99 | /** 100 | * @param string $json 101 | * 102 | * @return \Symfony\Component\HttpFoundation\Response 103 | */ 104 | protected function resourceProcessingResponse($json) 105 | { 106 | return $this->createResponse(new ResourceProcessing($json)); 107 | } 108 | 109 | /** 110 | * @param string $json 111 | * 112 | * @return \Symfony\Component\HttpFoundation\Response 113 | */ 114 | protected function resourceUpdatedResponse($json) 115 | { 116 | return $this->createResponse(new ResourceUpdated($json)); 117 | } 118 | 119 | /** 120 | * @param string $json 121 | * 122 | * @return \Symfony\Component\HttpFoundation\Response 123 | */ 124 | protected function response($json) 125 | { 126 | return $this->createResponse(new Response($json)); 127 | } 128 | 129 | /** 130 | * @param string $json 131 | * 132 | * @return \Symfony\Component\HttpFoundation\Response 133 | */ 134 | protected function unsupportedActionResponse($json) 135 | { 136 | $error = new Error('Unsupported Action', json_decode($json)); 137 | 138 | return $this->createResponse(new UnsupportedAction([$error])); 139 | } 140 | 141 | /** 142 | * @param $data 143 | * 144 | * @return \Symfony\Component\HttpFoundation\Response 145 | */ 146 | private function createResponse($data) 147 | { 148 | return (new HttpFoundationFactory())->createResponse($this->addHeaders($data)); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Serializer/JsonApiSerializer.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 8/22/15 6 | * Time: 12:33 PM. 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace NilPortugues\Symfony\JsonApiBundle\Serializer; 13 | 14 | use NilPortugues\Api\JsonApi\JsonApiTransformer; 15 | use NilPortugues\Api\Mapping\Mapping; 16 | use ReflectionClass; 17 | use RuntimeException; 18 | use Symfony\Bundle\FrameworkBundle\Routing\Router; 19 | use Exception; 20 | use Symfony\Component\HttpFoundation\Request; 21 | 22 | /** 23 | * Class JsonApiSerializer 24 | * @package NilPortugues\Symfony\JsonApiBundle\Serializer 25 | */ 26 | class JsonApiSerializer extends \NilPortugues\Api\JsonApi\JsonApiSerializer 27 | { 28 | /** 29 | * @param JsonApiTransformer $transformer 30 | * @param Router $router 31 | */ 32 | public function __construct(JsonApiTransformer $transformer, Router $router) 33 | { 34 | $this->mapUrls($transformer, $router); 35 | 36 | parent::__construct($transformer); 37 | } 38 | 39 | /** 40 | * @param JsonApiTransformer $transformer 41 | * @param Router $router 42 | */ 43 | private function mapUrls(JsonApiTransformer $transformer, Router $router) 44 | { 45 | $request = Request::createFromGlobals(); 46 | $baseUrl = $request->getSchemeAndHttpHost(); 47 | 48 | $reflectionClass = new ReflectionClass($transformer); 49 | $reflectionProperty = $reflectionClass->getProperty('mappings'); 50 | $reflectionProperty->setAccessible(true); 51 | $mappings = $reflectionProperty->getValue($transformer); 52 | 53 | foreach ($mappings as &$mapping) { 54 | $mappingClass = new ReflectionClass($mapping); 55 | 56 | $this->setUrlWithReflection($router, $mapping, $mappingClass, 'resourceUrlPattern', $baseUrl); 57 | $this->setUrlWithReflection($router, $mapping, $mappingClass, 'selfUrl', $baseUrl); 58 | 59 | $mappingProperty = $mappingClass->getProperty('otherUrls'); 60 | $mappingProperty->setAccessible(true); 61 | $otherUrls = $mappingProperty->getValue($mapping); 62 | if (empty($otherUrls)) { 63 | $otherUrls = []; 64 | } 65 | foreach ($otherUrls as &$url) { 66 | $url = $this->getUrlPattern($router, $url, $baseUrl); 67 | } 68 | $mappingProperty->setValue($mapping, $otherUrls); 69 | 70 | $mappingProperty = $mappingClass->getProperty('relationshipSelfUrl'); 71 | $mappingProperty->setAccessible(true); 72 | $relationshipSelfUrl = $mappingProperty->getValue($mapping); 73 | if (empty($relationshipSelfUrl)) { 74 | $relationshipSelfUrl = []; 75 | } 76 | foreach ($relationshipSelfUrl as &$urlMember) { 77 | foreach ($urlMember as &$url) { 78 | $url = $this->getUrlPattern($router, $url, $baseUrl); 79 | } 80 | } 81 | $mappingProperty->setValue($mapping, $relationshipSelfUrl); 82 | } 83 | 84 | $reflectionProperty->setValue($transformer, $mappings); 85 | } 86 | 87 | /** 88 | * @param Router $router 89 | * @param Mapping $mapping 90 | * @param ReflectionClass $mappingClass 91 | * @param string $property 92 | */ 93 | private function setUrlWithReflection(Router $router, Mapping $mapping, ReflectionClass $mappingClass, $property, $baseUrl) 94 | { 95 | $mappingProperty = $mappingClass->getProperty($property); 96 | $mappingProperty->setAccessible(true); 97 | $value = $mappingProperty->getValue($mapping); 98 | $value = $this->getUrlPattern($router, $value, $baseUrl); 99 | $mappingProperty->setValue($mapping, $value); 100 | } 101 | 102 | /** 103 | * @param Router $router 104 | * @param string $routeNameFromMappingFile 105 | * 106 | * @return mixed 107 | * 108 | * @throws RuntimeException 109 | */ 110 | private function getUrlPattern(Router $router, $routeNameFromMappingFile, $baseUrl) 111 | { 112 | if (!empty($routeNameFromMappingFile)) { 113 | try { 114 | $route = $router->getRouteCollection()->get($routeNameFromMappingFile); 115 | if (empty($route)) { 116 | throw new Exception(); 117 | } 118 | } catch (Exception $e) { 119 | throw new RuntimeException( 120 | \sprintf('Route \'%s\' has not been defined as a Symfony route.', $routeNameFromMappingFile) 121 | ); 122 | } 123 | 124 | \preg_match_all('/{(.*?)}/', $route->getPath(), $matches); 125 | 126 | $pattern = []; 127 | if (!empty($matches)) { 128 | $pattern = \array_combine($matches[1], $matches[0]); 129 | } 130 | 131 | return $baseUrl.\urldecode($router->generate($routeNameFromMappingFile, $pattern, true)); 132 | } 133 | 134 | return (string) $routeNameFromMappingFile; 135 | } 136 | } 137 | --------------------------------------------------------------------------------