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.

Samuel Gordalina

May 17, 2013
Tweet

More Decks by Samuel Gordalina

Other Decks in Programming

Transcript

  1. // 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; }
  2. // 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; }
  3. // 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; }
  4. // 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; }
  5. // 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; }
  6. // 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; }
  7. // 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; }
  8. // 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); }
  9. // 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); }
  10. // 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); }
  11. // 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); }
  12. // 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); }
  13. // 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); }
  14. 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(); }
  15. 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(); }
  16. 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(); }
  17. 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; }
  18. 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; }
  19. 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; }
  20. 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; }
  21. 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; }
  22. 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; }
  23. 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; }
  24. 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; }
  25. 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; }
  26. 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; }
  27. 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; }
  28. 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; }
  29. 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; }
  30. 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; }
  31. 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(); }
  32. 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(); }
  33. 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(); }
  34. 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(); }
  35. 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(); }
  36. 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(); }
  37. 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(); }
  38. 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
  39. 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)
  40. 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
  41. Roles public function postBlogPostAction() { if (false === $this->get('security.context')- >isGranted('ROLE_ADMIN'))

    { throw new HttpException(403, 'Forbidden'); } // application specific logic }
  42. 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); }
  43. 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); }
  44. 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); }
  45. 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); }
  46. ACL public function getBlogPostAction(Post $blogPost) { if (false === $this->get('security.context')-

    >isGranted('VIEW', $blogPost)) { throw new HttpException(403, 'Forbidden'); } // application specific logic }
  47. 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); }
  48. Inline Documentation /** * Get a Tweet * * **Response

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

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

    Format** * * { * "tweet": { * "id": 1, * "body": "the quick brown fox died", * } * } * * @ApiDoc( * section="Tweets", * resource=true, * statusCodes={ * 200="OK" * } * ) */
  51. 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