PHP Benelux - Build Restful API easily with Symfony

PHP Benelux - Build Restful API easily with Symfony

Let's build a nice REST API with all the recent helper we can find. First of all, we will take some time to see, in theory, what's asked to build a "good" Restfull API.
Then we'll see some code with the usage of :
FOSRestBundle, and save a lot of time;
JMS serializer and its awesome features;
the HATEOAS project, very nice to unlock the third level of the Richardson maturity model;
And finaly, a little bit of guzzle to ease communication between applications (going further with the SOA architecture).
The goal of that talk is to demystify all technologies in the "REST" galaxy.

34ade09dd3d11004ca8ee4174fd3d6a2?s=128

Sarah KHALIL

January 29, 2016
Tweet

Transcript

  1. Build Restful API easily with Symfony Sarah Khalil - January

    29th, 2016
  2. Who am I? • Head of • Trainer & Developer

    • Enjoying sharer Sarah Khalil @saro0h
  3. None
  4. What’s the plan?

  5. What’s the plan? Back to basics 1

  6. What’s the plan? Back to basics 1 How to do

    it? 2
  7. What’s the plan? Back to basics 1 Alternative tools 3

    How to do it? 2
  8. What is REST?

  9. What is REST? • REpresentational State Transfer • Architecture (not

    a protocol!) • Associated to Service Oriented Architecture • Management of resources
  10. None
  11. None
  12. None
  13. None
  14. The 6 constraints of REST

  15. Client - Server (1/6) Separation of concerns Portability

  16. Stateless (2/6) Based on HTTP Session has to be held

    on the client side
  17. Cacheable (3/6) Avoid useless requests

  18. Layered system (4/6) The client doesn’t know the system behind

  19. Uniform interface (5/6) Each resource has its identifier: the URI.

    Each resource has its representation. Auto-descripted message: for instance, JSON = content-type header.
  20. Code on demand (6/6) The client download some code and

    executes it The server is not aware of what is happening anymore optional
  21. HTTP Methods • GET • POST • PUT • PATCH

    • DELETE • OPTIONS • CONNECT • HEAD
  22. Restful API

  23. Keep’em in mind JSON, XML, HTML… By developers and for

    developers Scalability Latency Security !
  24. http://martinfowler.com

  25. HATEOAS Hypermedia as the engine of Application State Discoverability =>

    Links!
  26. None
  27. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle

  28. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle

  29. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle

  30. Request GET /users HTTP/1.1 User-Agent: curl/7.37.1 Host: 127.0.0.1:8000 Accept: */*

  31. Response HTTP/1.1 200 OK Host: 127.0.0.1:8000 Content-Type: text/html; charset=UTF-8 <!DOCTYPE

    html> <html lang="en"> <head>
  32. Symfony\Component\HttpFoundation\Request Symfony\Component\HttpFoundation\Response

  33. HttpKernel

  34. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle

  35. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle Work with resources

  36. None
  37. Json/XML Object serialization deserialization

  38. Configuration XML, YML and Annotation. http://jmsyst.com/libs/serializer/master/reference Helper to serialize properties

    the way you need.
  39. class DefaultController { public function indexAction() { $obj = new

    \StdClass(); $obj->latitude = -33.87087; $obj->longitude = 151.225459; $data = $this->get(‘jms_serializer’) ->serialize($obj, ’json’); return new Response( $data, 200, array(‘Content-Type’=>’application/json’) ); } }
  40. HTTP/1.1 200 OK Host: 127.0.0.1 X-Powered-By: PHP/5.6.3 Content-Type: application/json {

    "latitude": -33.87087, "longitude": 151.225459 } Output
  41. Exclusion policy /** * @JMS\ExclusionPolicy("all") */ class Product { /**

    * @JMS\Expose() */ private $name; private $users; } • Visitor pattern • Object graph • Relations (xToMany)
  42. Serializer handler Specific needs Implements JMS\Serializer\Handler\SubscribingHandlerInterface <tag name="jms_serializer.subscribing_handler" />

  43. None
  44. public static function getSubscribingMethods() { return [ [ 'direction' =>

    GraphNavigator::DIRECTION_SERIALIZATION, 'format' => ‘json', 'type' => ‘AppBundle\MyClass’, 'method' => ‘serialize', ], [ 'direction' => GraphNavigator::DIRECTION_DESERIALIZATION, 'format' => ‘json', 'type' => ‘AppBundle\MyClass', 'method' => ‘deserialize', ], ]; }
  45. public function serialize( JsonSerializationVisitor $visitor, MyClass $object, array $type, Context

    $context ) { $data = $object->toArray(); return $data; } public function deserialize(JsonDeserializationVisitor $visitor, $data) { $object = new AppBundle\MyClass($data); return $object; }
  46. Events

  47. Json/XML Object serialization deserialization serializer.pre_serialize serializer.post_serialize serializer.pre_deserialize serializer.post_deserialize

  48. class PostSerializeMediaListener implements EventSubscriberInterface { public function onPostSerialize(ObjectEvent $event) {

    $myObject = $event->getObject(); $event->getVisitor()->addData('newElementInJson', 'information’); } public static function getSubscribedEvents() { return array( array( 'event' => Events::POST_SERIALIZE, 'format' => ‘json', 'class' => ‘AppBundle\MyClass', 'method' => ‘onPostSerialize', ), ); } }
  49. Event subscriber <tag name="jms_serializer.event_subscriber" type="AppBundle\MyClass" direction="serialization" format="json" method="onPostSerialize" />

  50. Other features Serialization group Versionning

  51. None
  52. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle

  53. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle Setup your Symfony

    application
  54. None
  55. Controller # app/config/config.yml fos_rest: view: view_response_listener: force sensio_framework_extra: view: {

    annotations: false } /** * @View() */ public function getUsersAction() { … return $data; }
  56. Content-Type negociation • Request => Accept priorities of media-type(s) that

    your application is ready to work with. • Response => Content-Type the media type of the response (html, json, png…).
  57. Content-Type negociation Request: Accept text/json;q=0.9,*/*;q=0.8,text/html # app/config/config.yml fos_rest: format_listener: rules:

    - { path: '^/', priorities: ['json'], fallback_format: html } weighting
  58. Request body converter Decoder ParamConverter /** * @ParamConverter("post", converter="fos_rest.request_body") */

    public function updateAction(Post $post) { } # app/config/config.yml sensio_framework_extra: request: { converters: true } fos_rest: body_converter: enabled: true
  59. Validation

  60. Validation (with the Symfony Validator component) Configuration fos_rest: body_converter: enabled:

    true validate: true validation_errors_argument: validationErrors
  61. Validation (with the Symfony Validator component) Usage /** * @ParamConverter("post",

    converter="fos_rest.request_body") */ public function putPostAction(Post $post, ConstraintViolationListInterface $validationErrors) { if (count($validationErrors) > 0) { // Handle validation errors } }
  62. Param Fetcher

  63. /** * @QueryParam( * name="sort", * requirements="(asc|desc)", * allowBlank=false, *

    default="asc", * description="Sort direction" * ) * @RequestParam( * name="firstname", * requirements="[a-z]+" * ) * @QueryParam( * array=true, * name="filters", * requirements=@MyComplexConstraint * ) */ public function getArticlesAction(ParamFetcher $paramFetcher) {} http://mydomain.com/resources?sort=desc In $request->request ($_POST) fos_rest.param_fetcher_listener: true
  64. /** * @QueryParam(name="page", requirements="\d+", default="1") */ public function getArticlesAction($page) {}

    fos_rest.param_fetcher_listener: force
  65. Error handling

  66. Error handling Option 1 fos_rest: codes: 'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404 'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT

  67. fos_rest: view: exception_wrapper_handler: My\Bundle\Handler\MyExceptionWrapperHandler 1 Error handling Option 2

  68. fos_rest: view: exception_wrapper_handler: My\Bundle\Handler\MyExceptionWrapperHandler 1 implements FOS\RestBundle\ViewExceptionWrapperHandlerInterface 2 Error handling

    Option 2
  69. fos_rest: view: exception_wrapper_handler: My\Bundle\Handler\MyExceptionWrapperHandler 1 implements FOS\RestBundle\ViewExceptionWrapperHandlerInterface 2 public function

    wrap($data) { return new MyExceptionWrapper($data); } 3 Error handling Option 2
  70. fos_rest: view: exception_wrapper_handler: My\Bundle\Handler\MyExceptionWrapperHandler Take example on the FOS\RestBundle\Util\ExceptionWrapper 1

    implements FOS\RestBundle\ViewExceptionWrapperHandlerInterface 2 public function wrap($data) { return new MyExceptionWrapper($data); } 3 Error handling Option 2
  71. Other features • Automatic generation of routes • Consistency •

    Versioning • url • mime type And way more! http://symfony.com/doc/master/bundles/FOSRestBundle/index.html
  72. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle

  73. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle Unlock the level

    3 of the Richardson’s model
  74. None
  75. HAL • Hypertext Application Language • _links - relations •

    _embedded - related resource(s) • self • Specifications: http://stateless.co/hal_specification.html
  76. JSON API • Similar to HAL • meta - meta

    info such as pagination… • links - Relations (xToOne - xToMany) • linked - Resource(s) linked • Specifications : http://jsonapi.org/format/
  77. Configuration Annotation, XML and YML Relies on Expression Language (since

    Symfony 2.4)
  78. /** * @Hateoas\Relation( * "self", * href = @Hateoas\Route( *

    "get_notification", * absolute= true, * parameters = { * "id" = "expr(object.getId())", * "user" = "expr(service(‘security.context’).getToken().getUser().getUuid())" * } * ) * ) */ class Notification Expression Language
  79. Result { "id": 1, "_links": { "self": { "href": "http://domaine.name/users/eh1754329986/notifications/1/"

    } } }
  80. /** * … * @Hateoas\Relation( * "activity", * href =

    "expr(‘/activity/'~object.getActivity().getId()", * embedded = "expr(object.getActivity())", * exclusion = @Hateoas\Exclusion(excludeIf = "expr(object.getActivity() === null)") * ) */ class Notification
  81. Result { "id": 1, "_embedded": { "activity": { "id": 3,

    "content": "Activity content", "_links": { "self": { "href": "/activities/3" } } } } }
  82. Representation Use representation to decorate your resource Hateoas\Representation\PaginatedRepresentation And many

    more: https://github.com/willdurand/Hateoas/tree/master/src/Hateoas/Representation
  83. class CollectionRepresentation { private $resources; public function __construct($resources) { $this->resources

    = $resources; } public function getResources() { return $this->resources; } }
  84. "resources": { { "name": « Mercure Los Angeles" }, {

    "name": "Novotel Mexico" }, }
  85. Representation Any representation you need! A class with the properties

    you need Let the serializer do its job
  86. "page": 1, "limit": 10, "pages": "100", "total": 1000, "_links": {

    "self": { "href": "http://domain.name/notifcations?page=1&limit=10" }, "first": { "href": "http://domain.name/notifcations?page=1&limit=1" }, "last": { "href": "http://domain.name/notifcations?page=100&limit=1" }, "next": { "href": "http://domain.name/notifcations?page=2&limit=1" } }
  87. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle Guzzle

  88. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle Guzzle Communicate between

    microservices
  89. Frontend Service 1 Service 2 Service 3 Service 4 Private

    network Client
  90. Guzzle Bundle HTTP Client Request Response HTTPS Error management •

    https://github.com/misd- service-development/guzzle- bundle • https://github.com/csarrazi/ CsaGuzzleBundle
  91. None
  92. None
  93. Security

  94. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle

  95. Authentication & Authorization

  96. Authentication & Authorization

  97. Authentication & Authorization

  98. Authentication with multiple API’s

  99. Service 1 Service 2 Client Frontend 2 Frontend 1 Service

    3 Service 4
  100. None
  101. Workflow 1. The user authenticates [him|her]self on frontend 1 2.

    Do some stuff 3. Then clicks on a link that brings him/ her to the frontend 2
  102. Service 1 Service 2 Client Frontend 2 Frontend 1 Service

    3 Service 4 GET /admin HTTP/1.1 User-Agent: curl/7.37.1 Host: 127.0.0.1:8000 Accept: */* Authorization: Bearer XXXXXX
  103. Get the token In an authenticator, grab the Authorization header

    value
  104. Get the token public function createToken(Request $request, $providerKey) { $bearer

    = $request->headers… }
  105. Validate a token / Get a new one…

  106. None
  107. None
  108. Authentication • Authorization header: Authorization: Bearer <access_token> • https://github.com/poledev/katas/tree/kata- authentication

    • http://symfony.com/doc/current/cookbook/security/ api_key_authentication.html • Make it stateless!
  109. Authorization • access_control • $this->get(‘security.authorization_checker’)->isGranted('ROLE_ADMIN') • {% if is_granted(‘ROLE_ADMIN’) %}

    • Voter • …
  110. FosHttpCacheBundle Keep it mind

  111. None
  112. Ease the usage of your API

  113. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle

  114. Symfony Full Stack JMSSerializerBundle FOSRestBundle BazingaHateoasBundle NelmioApiDocBundle Documentation

  115. None
  116. None
  117. Alternative to FOSRestBundle

  118. https://api-platform.com/

  119. None
  120. None
  121. http://tinyurl.com/sfliveparis

  122. Thank you! @saro0h speakerdeck.com/saro0h/ github.com/saro0h/oauth-github Follow me on @catlannister Come

    see us at the SensioLabs booth!