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. REST APIs made
    easy with Symfony2
    Samuel Gordalina
    @sgordalina

    View Slide

  2. View Slide

  3. REST
    Roy Fielding’s PHD - 2000

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. CRUD

    View Slide

  9. Create
    HTTP POST

    View Slide

  10. Request
    POST /sgordalina/tweets HTTP/1.1
    Host: example.com
    Content-Type: application/json
    {
    "body": "the quick brown fox jumped"
    }

    View Slide

  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"
    }
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  19. # src/Twitter/ApiBundle/Resources/config/routing.yml
    tweet_post:
    pattern: /tweets
    defaults: { _controller:
    TwitterApiBundle:Tweet:post, _format: json }
    methods: POST

    View Slide

  20. Read
    HTTP GET

    View Slide

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

    View Slide

  22. Response
    HTTP/1.1 200 OK
    Content-Type: application/json
    {
    "tweet": {
    "id": 1,
    "body": "the quick brown fox jumped"
    }
    }

    View Slide

  23. Implementation
    public function getAction(Tweet $tweet)
    {
    return array('tweet' => $tweet);
    }

    View Slide

  24. Update
    HTTP PUT

    View Slide

  25. Request
    PUT /sgordalina/tweets/1 HTTP/1.1
    Host: example.com
    Content-Type: application/json
    {
    "body": "the quick brown fox died"
    }

    View Slide

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

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  33. Delete
    HTTP DELETE

    View Slide

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

    View Slide

  35. Response
    HTTP/1.1 204 No Content

    View Slide

  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();
    }

    View Slide

  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();
    }

    View Slide

  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();
    }

    View Slide

  39. Serialization
    Because everyone loves JSON

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  44. Serialization
    Because someone still uses XML

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  49. Output



    View Slide

  50. Deserialization
    With zero effort *

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  57. Errors
    Consumers will thank you

    View Slide

  58. Verbosity Matters

    View Slide

  59. View Slide

  60. Resource Linking
    The web is built on links

    View Slide

  61. LINK & UNLINK
    On HTTP 1.0

    View Slide

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

    View Slide

  63. Response
    HTTP 204 No Content

    View Slide

  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();
    }

    View Slide

  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();
    }

    View Slide

  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();
    }

    View Slide

  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();
    }

    View Slide

  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();
    }

    View Slide

  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();
    }

    View Slide

  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();
    }

    View Slide

  71. API Versioning
    Developer’s aspirin

    View Slide

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

    View Slide

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

    View Slide

  74. HTTP Header

    View Slide

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

    View Slide

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

    View Slide

  77. Authentication

    View Slide

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

    View Slide

  79. HTTP Basic
    Natively supported by Symfony2

    View Slide

  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

    View Slide

  81. Secret API Key
    A common implementation

    View Slide

  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"
    }

    View Slide

  83. WS-Security
    Web Services Security

    View Slide

  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)

    View Slide

  85. OAuth
    Standardized third party authentication

    View Slide

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

    View Slide

  87. Authorization

    View Slide

  88. Authorization
    •Role based authorization
    •ACL based authorization

    View Slide

  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

    View Slide

  90. Roles
    use JMS\SecurityExtraBundle\Annotation\PreAuthorize;
    /**
    * @PreAuthorize("hasRole('ROLE_WRITER')")
    */
    public function postBlogPostAction()
    {
    // application specific logic
    }

    View Slide

  91. Roles
    public function postBlogPostAction()
    {
    if (false === $this->get('security.context')-
    >isGranted('ROLE_ADMIN')) {
    throw new HttpException(403, 'Forbidden');
    }
    // application specific logic
    }

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  96. ACL
    use JMS\SecurityExtraBundle\Annotation\PreAuthorize;
    /**
    * @PreAuthorize("hasPermission(#blogPost, 'VIEW')")
    */
    public function getBlogPostAction(Post $blogPost)
    {
    // application specific logic
    }

    View Slide

  97. ACL
    public function getBlogPostAction(Post $blogPost)
    {
    if (false === $this->get('security.context')-
    >isGranted('VIEW', $blogPost)) {
    throw new HttpException(403, 'Forbidden');
    }
    // application specific logic
    }

    View Slide

  98. Documentation
    It is painful

    View Slide

  99. Nelmio ApiDocBundle

    View Slide

  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);
    }

    View Slide

  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"
    * }
    * )
    */

    View Slide

  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"
    * }
    * )
    */

    View Slide

  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"
    * }
    * )
    */

    View Slide

  104. Documentation

    View Slide

  105. Sandbox

    View Slide

  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

    View Slide

  107. Thank you!
    Samuel Gordalina
    @sgordalina
    #phpday 2013

    View Slide