Upgrade to Pro — share decks privately, control downloads, hide ads and more …

REST APIs made easy with Symfony2

REST APIs made easy with Symfony2

Samuel Gordalina
https://twitter.com/sgordalina
https://github.com/gordalina
https://gordalina.com

Symfony2 Application for this presentation
https://github.com/gordalina/sample-twitter-api-symfony2

REST is an architecture style for designing networked applications which has gained much traction lately, mainly due to its simplicity and for only requiring HTTP as the communication protocol.

This talk will leverage the extensibility and configurability of Symfony2 to easily create a reusable and testable REST API in minutes. Watch as we cover some real world examples and fill the gap where the abstraction layer falls short in providing the implementation for specific requirements.

We will dive into how data can be serialized & deserialized with little effort as possible whilst supporting different formats, namely JSON & XML. Authentication and Authorization will be visited in order to exemplify the security aspects of a real world API.

Sam will talk about how to document the API without effort and have that documentation generated automatically for end user consumption. Also we will fiddle with a web sandbox for instant access to the API.

Overall, Sam will talk about the different components that are necessary to build an API with ease.

A2624352f5141099905845086fd83789?s=128

Samuel Gordalina

May 17, 2013
Tweet

Transcript

  1. REST APIs made easy with Symfony2 Samuel Gordalina @sgordalina

  2. None
  3. REST Roy Fielding’s PHD - 2000

  4. REST http://example.com/sgordalina

  5. REST http://example.com/sgordalina http://example.com/sgordalina/tweets

  6. REST http://example.com/sgordalina http://example.com/sgordalina/tweets http://example.com/sgordalina/tweets/42

  7. What is needed •symfony/framework-standard-edition •friendsofsymfony/rest-bundle •jms/serializer-bundle •nelmio/api-doc-bundle

  8. CRUD

  9. Create HTTP POST

  10. Request POST /sgordalina/tweets HTTP/1.1 Host: example.com Content-Type: application/json { "body":

    "the quick brown fox jumped" }
  11. Response HTTP/1.1 201 Created Location: http://example.com/sgordalina/tweets/1 Content-Type: application/json { "tweet":

    { "id": 1, "body": "the quick brown fox jumped" } }
  12. // src/Twitter/ApiBundle/Controller/TweetController.php use FOS\RestBundle\View\View; public function postAction(Request $request) { $tweet

    = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($tweet instanceof Tweet === false) { return View::create(array('errors' => $tweet), 400); } $em = $this->getEntityManager(); $em->persist($tweet); $em->flush(); $url = $this->generateUrl( 'tweet_get', array('id' => $tweet->getId()), true ); $response = new Response(); $response->setStatusCode(201); $response->headers->set('Location', $url); return $response; }
  13. // src/Twitter/ApiBundle/Controller/TweetController.php use FOS\RestBundle\View\View; public function postAction(Request $request) { $tweet

    = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($tweet instanceof Tweet === false) { return View::create(array('errors' => $tweet), 400); } $em = $this->getEntityManager(); $em->persist($tweet); $em->flush(); $url = $this->generateUrl( 'tweet_get', array('id' => $tweet->getId()), true ); $response = new Response(); $response->setStatusCode(201); $response->headers->set('Location', $url); return $response; }
  14. // src/Twitter/ApiBundle/Controller/TweetController.php use FOS\RestBundle\View\View; public function postAction(Request $request) { $tweet

    = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($tweet instanceof Tweet === false) { return View::create(array('errors' => $tweet), 400); } $em = $this->getEntityManager(); $em->persist($tweet); $em->flush(); $url = $this->generateUrl( 'tweet_get', array('id' => $tweet->getId()), true ); $response = new Response(); $response->setStatusCode(201); $response->headers->set('Location', $url); return $response; }
  15. // src/Twitter/ApiBundle/Controller/TweetController.php use FOS\RestBundle\View\View; public function postAction(Request $request) { $tweet

    = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($tweet instanceof Tweet === false) { return View::create(array('errors' => $tweet), 400); } $em = $this->getEntityManager(); $em->persist($tweet); $em->flush(); $url = $this->generateUrl( 'tweet_get', array('id' => $tweet->getId()), true ); $response = new Response(); $response->setStatusCode(201); $response->headers->set('Location', $url); return $response; }
  16. // src/Twitter/ApiBundle/Controller/TweetController.php use FOS\RestBundle\View\View; public function postAction(Request $request) { $tweet

    = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($tweet instanceof Tweet === false) { return View::create(array('errors' => $tweet), 400); } $em = $this->getEntityManager(); $em->persist($tweet); $em->flush(); $url = $this->generateUrl( 'tweet_get', array('id' => $tweet->getId()), true ); $response = new Response(); $response->setStatusCode(201); $response->headers->set('Location', $url); return $response; }
  17. // src/Twitter/ApiBundle/Controller/TweetController.php use FOS\RestBundle\View\View; public function postAction(Request $request) { $tweet

    = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($tweet instanceof Tweet === false) { return View::create(array('errors' => $tweet), 400); } $em = $this->getEntityManager(); $em->persist($tweet); $em->flush(); $url = $this->generateUrl( 'tweet_get', array('id' => $tweet->getId()), true ); $response = new Response(); $response->setStatusCode(201); $response->headers->set('Location', $url); return $response; }
  18. // src/Twitter/ApiBundle/Controller/TweetController.php use FOS\RestBundle\View\View; public function postAction(Request $request) { $tweet

    = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($tweet instanceof Tweet === false) { return View::create(array('errors' => $tweet), 400); } $em = $this->getEntityManager(); $em->persist($tweet); $em->flush(); $url = $this->generateUrl( 'tweet_get', array('id' => $tweet->getId()), true ); $response = new Response(); $response->setStatusCode(201); $response->headers->set('Location', $url); return $response; }
  19. # src/Twitter/ApiBundle/Resources/config/routing.yml tweet_post: pattern: /tweets defaults: { _controller: TwitterApiBundle:Tweet:post, _format:

    json } methods: POST
  20. Read HTTP GET

  21. Request GET /sgordalina/tweets/1 HTTP/1.1 Host: example.com

  22. Response HTTP/1.1 200 OK Content-Type: application/json { "tweet": { "id":

    1, "body": "the quick brown fox jumped" } }
  23. Implementation public function getAction(Tweet $tweet) { return array('tweet' => $tweet);

    }
  24. Update HTTP PUT

  25. Request PUT /sgordalina/tweets/1 HTTP/1.1 Host: example.com Content-Type: application/json { "body":

    "the quick brown fox died" }
  26. Response HTTP/1.1 200 OK Content-Type: application/json { "tweet": { "id":

    1, "body": "the quick brown fox died" } }
  27. // TweetController.php public function putAction( Tweet $tweet, Request $request )

    { $newTweet = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($newTweet instanceof Tweet === false) { return View::create(array( 'errors' => $newTweet ), 400); } $tweet->merge($newTweet); $this->getEntityManager()->flush(); return array('tweet' => $tweet); }
  28. // TweetController.php public function putAction( Tweet $tweet, Request $request )

    { $newTweet = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($newTweet instanceof Tweet === false) { return View::create(array( 'errors' => $newTweet ), 400); } $tweet->merge($newTweet); $this->getEntityManager()->flush(); return array('tweet' => $tweet); }
  29. // TweetController.php public function putAction( Tweet $tweet, Request $request )

    { $newTweet = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($newTweet instanceof Tweet === false) { return View::create(array( 'errors' => $newTweet ), 400); } $tweet->merge($newTweet); $this->getEntityManager()->flush(); return array('tweet' => $tweet); }
  30. // TweetController.php public function putAction( Tweet $tweet, Request $request )

    { $newTweet = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($newTweet instanceof Tweet === false) { return View::create(array( 'errors' => $newTweet ), 400); } $tweet->merge($newTweet); $this->getEntityManager()->flush(); return array('tweet' => $tweet); }
  31. // TweetController.php public function putAction( Tweet $tweet, Request $request )

    { $newTweet = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($newTweet instanceof Tweet === false) { return View::create(array( 'errors' => $newTweet ), 400); } $tweet->merge($newTweet); $this->getEntityManager()->flush(); return array('tweet' => $tweet); }
  32. // TweetController.php public function putAction( Tweet $tweet, Request $request )

    { $newTweet = $this->deserialize( 'Twitter\DomainBundle\Entity\Tweet', $request ); if ($newTweet instanceof Tweet === false) { return View::create(array( 'errors' => $newTweet ), 400); } $tweet->merge($newTweet); $this->getEntityManager()->flush(); return array('tweet' => $tweet); }
  33. Delete HTTP DELETE

  34. Request DELETE /sgordalina/tweets/1 HTTP/1.1 Host: example.com

  35. Response HTTP/1.1 204 No Content

  36. use FOS\RestBundle\Controller\Annotations\View as RestView; /** * Delete a Tweet *

    * @RestView(statusCode=204) */ public function deleteAction(Tweet $tweet) { $em = $this->getDoctrine()->getManager(); $em->remove($tweet); $em->flush(); }
  37. use FOS\RestBundle\Controller\Annotations\View as RestView; /** * Delete a Tweet *

    * @RestView(statusCode=204) */ public function deleteAction(Tweet $tweet) { $em = $this->getDoctrine()->getManager(); $em->remove($tweet); $em->flush(); }
  38. use FOS\RestBundle\Controller\Annotations\View as RestView; /** * Delete a Tweet *

    * @RestView(statusCode=204) */ public function deleteAction(Tweet $tweet) { $em = $this->getDoctrine()->getManager(); $em->remove($tweet); $em->flush(); }
  39. Serialization Because everyone loves JSON

  40. use Symfony\Component\Validator\Constraints as Assert; use JMS\Serializer\Annotation as Serializer; /** *

    @Serializer\ExclusionPolicy("all") */ class Tweet { /** * @Serializer\Expose * @Serializer\Type("integer") */ private $id; /** * @Assert\NotBlank() * @Serializer\Expose * @Serializer\Type("string") */ private $body; }
  41. use Symfony\Component\Validator\Constraints as Assert; use JMS\Serializer\Annotation as Serializer; /** *

    @Serializer\ExclusionPolicy("all") */ class Tweet { /** * @Serializer\Expose * @Serializer\Type("integer") */ private $id; /** * @Assert\NotBlank() * @Serializer\Expose * @Serializer\Type("string") */ private $body; }
  42. use Symfony\Component\Validator\Constraints as Assert; use JMS\Serializer\Annotation as Serializer; /** *

    @Serializer\ExclusionPolicy("all") */ class Tweet { /** * @Serializer\Expose * @Serializer\Type("integer") */ private $id; /** * @Assert\NotBlank() * @Serializer\Expose * @Serializer\Type("string") */ private $body; }
  43. use Symfony\Component\Validator\Constraints as Assert; use JMS\Serializer\Annotation as Serializer; /** *

    @Serializer\ExclusionPolicy("all") */ class Tweet { /** * @Serializer\Expose * @Serializer\Type("integer") */ private $id; /** * @Assert\NotBlank() * @Serializer\Expose * @Serializer\Type("string") */ private $body; }
  44. Serialization Because someone still uses XML

  45. use Symfony\Component\Validator\Constraints as Assert; use JMS\Serializer\Annotation as Serializer; /** *

    @Serializer\ExclusionPolicy("all") * @Serializer\XmlRoot("tweet") */ class Tweet { /** * @var integer * * @Serializer\Expose * @Serializer\Type("integer") * @Serializer\XmlAttribute */ private $id; /** * @Assert\NotBlank() * @Serializer\Expose * @Serializer\Type("string") * @Serializer\Value */ private $body; }
  46. use Symfony\Component\Validator\Constraints as Assert; use JMS\Serializer\Annotation as Serializer; /** *

    @Serializer\ExclusionPolicy("all") * @Serializer\XmlRoot("tweet") */ class Tweet { /** * @var integer * * @Serializer\Expose * @Serializer\Type("integer") * @Serializer\XmlAttribute */ private $id; /** * @Assert\NotBlank() * @Serializer\Expose * @Serializer\Type("string") * @Serializer\Value */ private $body; }
  47. use Symfony\Component\Validator\Constraints as Assert; use JMS\Serializer\Annotation as Serializer; /** *

    @Serializer\ExclusionPolicy("all") * @Serializer\XmlRoot("tweet") */ class Tweet { /** * @var integer * * @Serializer\Expose * @Serializer\Type("integer") * @Serializer\XmlAttribute */ private $id; /** * @Assert\NotBlank() * @Serializer\Expose * @Serializer\Type("string") * @Serializer\Value */ private $body; }
  48. use Symfony\Component\Validator\Constraints as Assert; use JMS\Serializer\Annotation as Serializer; /** *

    @Serializer\ExclusionPolicy("all") * @Serializer\XmlRoot("tweet") */ class Tweet { /** * @var integer * * @Serializer\Expose * @Serializer\Type("integer") * @Serializer\XmlAttribute */ private $id; /** * @Assert\NotBlank() * @Serializer\Expose * @Serializer\Type("string") * @Serializer\Value */ private $body; }
  49. Output <tweet id="1"> <![CDATA[ the quick brown fox died ]]>

    </tweet>
  50. Deserialization With zero effort *

  51. protected function deserialize( $class, Request $request, $format = 'json' )

    { $serializer = $this->get('serializer'); $validator = $this->get('validator'); try { $entity = $serializer->deserialize( $request->getContent(), $class, $format ); } catch (RuntimeException $e) { throw new HttpException(400, $e->getMessage()); } if (count($errors = $validator->validate($entity))) { return $errors; } return $entity; }
  52. protected function deserialize( $class, Request $request, $format = 'json' )

    { $serializer = $this->get('serializer'); $validator = $this->get('validator'); try { $entity = $serializer->deserialize( $request->getContent(), $class, $format ); } catch (RuntimeException $e) { throw new HttpException(400, $e->getMessage()); } if (count($errors = $validator->validate($entity))) { return $errors; } return $entity; }
  53. protected function deserialize( $class, Request $request, $format = 'json' )

    { $serializer = $this->get('serializer'); $validator = $this->get('validator'); try { $entity = $serializer->deserialize( $request->getContent(), $class, $format ); } catch (RuntimeException $e) { throw new HttpException(400, $e->getMessage()); } if (count($errors = $validator->validate($entity))) { return $errors; } return $entity; }
  54. protected function deserialize( $class, Request $request, $format = 'json' )

    { $serializer = $this->get('serializer'); $validator = $this->get('validator'); try { $entity = $serializer->deserialize( $request->getContent(), $class, $format ); } catch (RuntimeException $e) { throw new HttpException(400, $e->getMessage()); } if (count($errors = $validator->validate($entity))) { return $errors; } return $entity; }
  55. protected function deserialize( $class, Request $request, $format = 'json' )

    { $serializer = $this->get('serializer'); $validator = $this->get('validator'); try { $entity = $serializer->deserialize( $request->getContent(), $class, $format ); } catch (RuntimeException $e) { throw new HttpException(400, $e->getMessage()); } if (count($errors = $validator->validate($entity))) { return $errors; } return $entity; }
  56. protected function deserialize( $class, Request $request, $format = 'json' )

    { $serializer = $this->get('serializer'); $validator = $this->get('validator'); try { $entity = $serializer->deserialize( $request->getContent(), $class, $format ); } catch (RuntimeException $e) { throw new HttpException(400, $e->getMessage()); } if (count($errors = $validator->validate($entity))) { return $errors; } return $entity; }
  57. Errors Consumers will thank you

  58. Verbosity Matters

  59. None
  60. Resource Linking The web is built on links

  61. LINK & UNLINK On HTTP 1.0

  62. Request LINK /sgordalina/following HTTP/1.1 Host: example.com Link: <http://example.com/phpday>; rel=”friend”

  63. Response HTTP 204 No Content

  64. public function followAction(Request $request) { if (false === $request->attributes->has('link')) {

    throw new HttpException(400, 'Link not provided'); } $me = $this->getUser(); foreach ($request->attributes->get('link') as $user) { if (false === $user instanceof User) { throw new HttpException(404, 'User not found'); } if (false === $me->isFollowing($user)) { $me->followUser($user); } } $this->getDoctrine() ->getManager() ->flush(); }
  65. public function followAction(Request $request) { if (false === $request->attributes->has('link')) {

    throw new HttpException(400, 'Link not provided'); } $me = $this->getUser(); foreach ($request->attributes->get('link') as $user) { if (false === $user instanceof User) { throw new HttpException(404, 'User not found'); } if (false === $me->isFollowing($user)) { $me->followUser($user); } } $this->getDoctrine() ->getManager() ->flush(); }
  66. public function followAction(Request $request) { if (false === $request->attributes->has('link')) {

    throw new HttpException(400, 'Link not provided'); } $me = $this->getUser(); foreach ($request->attributes->get('link') as $user) { if (false === $user instanceof User) { throw new HttpException(404, 'User not found'); } if (false === $me->isFollowing($user)) { $me->followUser($user); } } $this->getDoctrine() ->getManager() ->flush(); }
  67. public function followAction(Request $request) { if (false === $request->attributes->has('link')) {

    throw new HttpException(400, 'Link not provided'); } $me = $this->getUser(); foreach ($request->attributes->get('link') as $user) { if (false === $user instanceof User) { throw new HttpException(404, 'User not found'); } if (false === $me->isFollowing($user)) { $me->followUser($user); } } $this->getDoctrine() ->getManager() ->flush(); }
  68. public function followAction(Request $request) { if (false === $request->attributes->has('link')) {

    throw new HttpException(400, 'Link not provided'); } $me = $this->getUser(); foreach ($request->attributes->get('link') as $user) { if (false === $user instanceof User) { throw new HttpException(404, 'User not found'); } if (false === $me->isFollowing($user)) { $me->followUser($user); } } $this->getDoctrine() ->getManager() ->flush(); }
  69. public function followAction(Request $request) { if (false === $request->attributes->has('link')) {

    throw new HttpException(400, 'Link not provided'); } $me = $this->getUser(); foreach ($request->attributes->get('link') as $user) { if (false === $user instanceof User) { throw new HttpException(404, 'User not found'); } if (false === $me->isFollowing($user)) { $me->followUser($user); } } $this->getDoctrine() ->getManager() ->flush(); }
  70. public function followAction(Request $request) { if (false === $request->attributes->has('link')) {

    throw new HttpException(400, 'Link not provided'); } $me = $this->getUser(); foreach ($request->attributes->get('link') as $user) { if (false === $user instanceof User) { throw new HttpException(404, 'User not found'); } if (false === $me->isFollowing($user)) { $me->followUser($user); } } $this->getDoctrine() ->getManager() ->flush(); }
  71. API Versioning Developer’s aspirin

  72. API Versioning •URL Versioning •HTTP Header •HATEOAS

  73. URL Versioning •http://example.com/v1/sgordalina/tweets •http://v1.example.com/sgordalina/tweets

  74. HTTP Header

  75. HTTP Header GET /sgordalina/tweets/1 HTTP/1.1 Host: example.com API-Version: 1.0

  76. HATEOAS GET /sgordalina/tweets/1 HTTP/1.1 Host: example.com Accept: application/vnd.example.tweet-v1+json

  77. Authentication

  78. Authentication •HTTP Basic •WS-Security •Secret API Key •OAuth

  79. HTTP Basic Natively supported by Symfony2

  80. HTTP Basic # app/config/security.yml security: providers: in_memory: memory: users: admin:

    { password: password } firewalls: rest_api: pattern: /api/.* stateless: true http_basic: provider: in_memory
  81. Secret API Key A common implementation

  82. Secret API Key POST /sgordalina/tweets HTTP/1.1 Host: example.com Api-Secret-Key: kUYbs72gf83034

    { "body": "the quick brown fox jumped over the espresso" }
  83. WS-Security Web Services Security

  84. WS-Security •Authentication is sent as an header •Credentials are hashed

    in a digest with a nonce and creation time •Example cookbook on Symfony 2 documentation (custom authentication provider)
  85. OAuth Standardized third party authentication

  86. OAuth •Delegate authentication to other services •HWIOAuthBundle •Supports many providers

    •Integrates with FOSUserBundle
  87. Authorization

  88. Authorization •Role based authorization •ACL based authorization

  89. Roles # app/config/security.yml security: providers: in_memory: memory: users: admin: password:

    password roles: [ 'ROLE_ADMIN' ] writer: password: password roles: [ 'ROLE_WRITER' ] reader: password: password roles: [ 'ROLE_READER' ] role_hierarchy: ROLE_READER: ~ ROLE_WRITER: ROLE_READER ROLE_ADMIN: ROLE_WRITER
  90. Roles use JMS\SecurityExtraBundle\Annotation\PreAuthorize; /** * @PreAuthorize("hasRole('ROLE_WRITER')") */ public function postBlogPostAction()

    { // application specific logic }
  91. Roles public function postBlogPostAction() { if (false === $this->get('security.context')- >isGranted('ROLE_ADMIN'))

    { throw new HttpException(403, 'Forbidden'); } // application specific logic }
  92. ACL public function postBlogPostAction() { $blogPost = // application specific

    logic $aclProvider = $this->get('security.acl.provider'); $objectIdentity = ObjectIdentity::fromDomainObject($blogPost); $userSecurityIdentity = UserSecurityIdentity::fromAccount($this->getUser); $acl = $aclProvider->createAcl($objectIdentity); $acl->insertObjectAce($userSecurityIdentity, MaskBuilder::MASK_OWNER); $aclProvider->updateAcl($acl); }
  93. ACL public function postBlogPostAction() { $blogPost = // application specific

    logic $aclProvider = $this->get('security.acl.provider'); $objectIdentity = ObjectIdentity::fromDomainObject($blogPost); $userSecurityIdentity = UserSecurityIdentity::fromAccount($this->getUser); $acl = $aclProvider->createAcl($objectIdentity); $acl->insertObjectAce($userSecurityIdentity, MaskBuilder::MASK_OWNER); $aclProvider->updateAcl($acl); }
  94. ACL public function postBlogPostAction() { $blogPost = // application specific

    logic $aclProvider = $this->get('security.acl.provider'); $objectIdentity = ObjectIdentity::fromDomainObject($blogPost); $userSecurityIdentity = UserSecurityIdentity::fromAccount($this->getUser); $acl = $aclProvider->createAcl($objectIdentity); $acl->insertObjectAce($userSecurityIdentity, MaskBuilder::MASK_OWNER); $aclProvider->updateAcl($acl); }
  95. ACL public function postBlogPostAction() { $blogPost = // application specific

    logic $aclProvider = $this->get('security.acl.provider'); $objectIdentity = ObjectIdentity::fromDomainObject($blogPost); $userSecurityIdentity = UserSecurityIdentity::fromAccount($this->getUser); $acl = $aclProvider->createAcl($objectIdentity); $acl->insertObjectAce($userSecurityIdentity, MaskBuilder::MASK_OWNER); $aclProvider->updateAcl($acl); }
  96. ACL use JMS\SecurityExtraBundle\Annotation\PreAuthorize; /** * @PreAuthorize("hasPermission(#blogPost, 'VIEW')") */ public function

    getBlogPostAction(Post $blogPost) { // application specific logic }
  97. ACL public function getBlogPostAction(Post $blogPost) { if (false === $this->get('security.context')-

    >isGranted('VIEW', $blogPost)) { throw new HttpException(403, 'Forbidden'); } // application specific logic }
  98. Documentation It is painful

  99. Nelmio ApiDocBundle

  100. Inline Documentation use Nelmio\ApiDocBundle\Annotation\ApiDoc; /** * Get a Tweet *

    * **Response Format** * * { * "tweet": { * "id": 1, * "body": "the quick brown fox died", * } * } * * @ApiDoc( * section="Tweets", * resource=true, * statusCodes={ * 200="OK" * } * ) */ public function getAction(Tweet $tweet) { return array('tweet' => $tweet); }
  101. Inline Documentation /** * Get a Tweet * * **Response

    Format** * * { * "tweet": { * "id": 1, * "body": "the quick brown fox died", * } * } * * @ApiDoc( * section="Tweets", * resource=true, * statusCodes={ * 200="OK" * } * ) */
  102. Inline Documentation /** * Get a Tweet * * **Response

    Format** * * { * "tweet": { * "id": 1, * "body": "the quick brown fox died", * } * } * * @ApiDoc( * section="Tweets", * resource=true, * statusCodes={ * 200="OK" * } * ) */
  103. Inline Documentation /** * Get a Tweet * * **Response

    Format** * * { * "tweet": { * "id": 1, * "body": "the quick brown fox died", * } * } * * @ApiDoc( * section="Tweets", * resource=true, * statusCodes={ * 200="OK" * } * ) */
  104. Documentation

  105. Sandbox

  106. Additional Resources • LinkRequest Listener • https://gist.github.com/gordalina/5597794 • WSS Authentication

    Provider • http://symfony.com/doc/current/cookbook/security/ custom_authentication_provider.html • Bundles • https://github.com/FriendsOfSymfony/FOSRestBundle • https://github.com/nelmio/NelmioApiDocBundle • https://github.com/schmittjoh/JMSSerializerBundle
  107. Thank you! Samuel Gordalina @sgordalina #phpday 2013