Pro Yearly is on sale from $80 to $50! »

Building RESTful APIs with Symfony Components

Building RESTful APIs with Symfony Components

Built around the HTTP specification, Symfony components provide a rock solid foundation for building RESTful APIs, whether we are using the full framework or not. In this session we will see how to successfully deal with problems such as how to adapt our models to the desired representations back and forth, authentication strategies, some tips and tricks to test effectively our APIs and also how to apply these concepts to migrate effectively a legacy application, based upon real use cases.

281604c1a5357a164f2a9cd6e403b4e3?s=128

Victoria Quirante

February 16, 2017
Tweet

Transcript

  1. Building RESTful APIs with Symfony Components Victoria Quirante 16-17th Feb.

    2017
  2. I work at Limenius We build tailor-made projects with Symfony

    and React Most projects require the implementation of an API Symfony Components provide a rock-solid foundation for building APIs Victoria Quirante @vicqr victoria@limenius.com
  3. I. INTRODUCTION

  4. Why would I want my API to be RESTful?

  5. Possible reasons AGAINST Perhaps I can think of a better

    way to structure an API
  6. Possible reasons AGAINST Perhaps I can think of a better

    way to structure an API REST seems very controversial and confusing in some points
  7. Main reasons to go for REST REST makes the most

    of HTTP
  8. Main reasons to go for REST REST makes the most

    of HTTP It means to have a common language
  9. Main reasons to go for REST REST makes the most

    of HTTP It means to have a common language These are very powerful reasons
  10. What you already know about REST

  11. Sane way to approach REST

  12. Sane way to approach REST 1. Learn the stuff that

    is commonly accepted
  13. Sane way to approach REST 1. Learn the stuff that

    is commonly accepted Use nouns for the resources!
  14. Sane way to approach REST 1. Learn the stuff that

    is commonly accepted Use nouns for the resources! Use HTTP verbs!
  15. Sane way to approach REST 1. Learn the stuff that

    is commonly accepted Use HTTP verbs! Use nouns for the resources! Return meaningful status codes!
  16. Sane way to approach REST 1. Learn the stuff that

    is commonly accepted 2. Be aware of the grey areas / controversial points
  17. Sane way to approach REST 1. Learn the stuff that

    is commonly accepted 2. Be aware of the grey areas / controversial points Do I have to return the created/updated resource?
  18. Sane way to approach REST 1. Learn the stuff that

    is commonly accepted 2. Be aware of the grey areas / controversial points Do I have to return the created/updated resource? Can I have /recipes.json and /recipes.html?
  19. Sane way to approach REST “Remember the "RE" in REST

    stands for "REpresentational" which means we are talking about representation of state. Therefore a single URI should be used by resource as the unique identifier. Instead to get different representations we should use media types.” Lukas K. Smith (FOSRestBundle author) http://pooteeweet.org/blog/2248 Can I have /recipes.json and /recipes.html?
  20. Sane way to approach REST “Remember the "RE" in REST

    stands for "REpresentational" which means we are talking about representation of state. Therefore a single URI should be used by resource as the unique identifier. Instead to get different representations we should use media types.” Lukas K. Smith (FOSRestBundle author) http://pooteeweet.org/blog/2248 “Section 6.2.1 does not say that content negotiation should be used all the time.” Roy T. Fielding (author of REST thesis and REST concept itself) https://groups.yahoo.com/neo/groups/rest-discuss/conversations/messages/5857 Can I have /recipes.json and /recipes.html?
  21. Sane way to approach REST “Remember the "RE" in REST

    stands for "REpresentational" which means we are talking about representation of state. Therefore a single URI should be used by resource as the unique identifier. Instead to get different representations we should use media types.” Lukas K. Smith (FOSRestBundle author) http://pooteeweet.org/blog/2248 “Section 6.2.1 does not say that content negotiation should be used all the time.” Roy T. Fielding (author of REST thesis and REST concept itself) https://groups.yahoo.com/neo/groups/rest-discuss/conversations/messages/5857 Can I have /recipes.json and /recipes.html?
  22. Sane way to approach REST 1. Learn the stuff that

    is commonly accepted 2. Be aware of the grey areas / controversial points 3. Choose a side in those…
  23. Sane way to approach REST 1. Learn the stuff that

    is commonly accepted 2. Be aware of the grey areas / controversial points 3. Choose a side in those… … and stick to it
  24. Sane way to approach REST 1. Learn the stuff that

    is commonly accepted 2. Be aware of the grey areas / controversial points 3. Choose a side in those… … and stick to it Be civilized and consistent
  25. None
  26. None
  27. Why Symfony & REST?

  28. Why Symfony & REST? REST is several things: - A

    very strict definition -that not many people fully understand - A battlefield - A way to make the most of HTTP
  29. Why Symfony & REST? REST is several things: - A

    very strict definition -that not many people fully understand - A battlefield - A way to make the most of HTTP “I don't like MVC because that's not how the web works. Symfony2 is an HTTP framework; it is a Request/Response framework. That's the big deal. The fundamental principles of Symfony2 are centered around the HTTP specification.” Fabien Potencier
  30. Why Symfony & REST? REST ♥ HTTP Symfony ♥ HTTP

  31. Why Symfony Components?

  32. Symfony is two things 1. A full stack framework

  33. Symfony is two things 1. A full stack framework 2.

    A set of independent components
  34. What are Symfony Components Set of decoupled and reusable PHP

    libraries You can use any in your own applications independently from the Symfony Framework Many popular PHP projects do so
  35. Components used in popular projects http://symfony.com/projects

  36. Doctrine http://symfony.com/projects

  37. Propel http://symfony.com/projects

  38. Silex http://symfony.com/projects

  39. Drupal http://symfony.com/projects

  40. Laravel http://symfony.com/projects

  41. Components that we are going to see in detail HttpFoundation

    Serializer Validator Form Guard
  42. Where/when can you apply this knowledge - Working with the

    full Symfony framework
  43. Where/when can you apply this knowledge - Working with the

    full Symfony framework - Using some of these components in some other framework
  44. Where/when can you apply this knowledge - Working with the

    full Symfony framework - Using some of these components in some other framework - Writing your own framework
  45. Where/when can you apply this knowledge - Working with the

    full Symfony framework - Using some of these components in some other framework - Writing your own framework https://symfony.com/doc/current/create_framework/index.html
  46. See repository https://github.com/VictoriaQ/rest-symfony-components

  47. II. SYMFONY COMPONENTS

  48. HttpFoundation Moving to an Object-Oriented approach HttpFoundation Serializer Validator Form

    Guard
  49. HttpFoundation - The idea Defines an object-oriented layer for the

    HTTP specification
  50. HttpFoundation - The idea In PHP: - Request represented by

    global variables - Response generated by some functions Defines an object-oriented layer for the HTTP specification
  51. HttpFoundation - The idea In PHP: - Request represented by

    global variables - Response generated by some functions HttpFoundation replaces global variables and functions by an object-oriented layer: - $_GET, $_POST, $_FILES, $_COOKIE... ----> Request() - echo(), header(), setcookie()... ----> Response() Defines an object-oriented layer for the HTTP specification
  52. HttpFoundation - Request() Holds information about the client request use

    Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals();
  53. HttpFoundation - Request() Creates Request object based on current PHP

    global variables use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals();
  54. HttpFoundation - Request() Creates Request object based on current PHP

    global variables use Symfony\Component\HttpFoundation\Request; $request = new Request( $_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER );
  55. HttpFoundation - Response() Holds information of what needs to be

    sent back to the client use Symfony\Component\HttpFoundation\Response; $response = new Response( 'Content', Response::HTTP_OK, array('content-type' => 'text/html') );
  56. Let’s create a POST endpoint When I request "POST /recipes"

    Then the response status code should be 201
  57. Let’s create a POST endpoint use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals();

    $content = json_decode($request->getContent(), true);
  58. Let’s create a POST endpoint use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; $request

    = Request::createFromGlobals(); $content = json_decode($request->getContent(), true); // Here we would persist the data $response = new Response(json_encode(array('data' => 'Hi!'), 201); $response->headers->set('Content-Type', 'application/json'); $response->headers->set('Location', '/recipes/1');
  59. Shortcut: JsonResponse() use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\JsonResponse; $request = Request::createFromGlobals(); $content

    = json_decode($request->getContent(), true); // Here we would persist the data $response = new JsonResponse(array('data' => 'Hi!'), 201); $response->headers->set('Location', '/recipes/1'); Sets ContentType to application/json and encodes to JSON
  60. And what about PSR-7?

  61. Symfony PSR-7 Bridge HttpFoundation has helped with homogenization across frameworks

    The PSR-7 Standard has meant a step further towards standardization Symfony 4 will likely embrace the PSR-7 Standard
  62. Symfony PSR-7 Bridge HttpFoundation has helped with homogenization across frameworks

    The PSR-7 Standard has meant a step further towards standardization Symfony 4 will likely embrace the PSR-7 Standard Until then, the Symfony PSR-7 Bridge makes HttpFoundation objects compatible with PSR-7
  63. Symfony PSR-7 Bridge use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; $psr7Factory

    = new DiactorosFactory(); // convert a Request $symfonyRequest = Request::createFromGlobals(); $psrRequest = $psr7Factory->createRequest($symfonyRequest); // convert a Response $symfonyResponse = new Response('Content'); $psrResponse = $psr7Factory->createResponse($symfonyResponse); This is how we can convert HttpFoundation objects to objects implementing HTTP message interfaces defined by the PSR-7
  64. Symfony PSR-7 Bridge use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; $psr7Factory

    = new DiactorosFactory(); // convert a Request $symfonyRequest = Request::createFromGlobals(); $psrRequest = $psr7Factory->createRequest($symfonyRequest); // convert a Response $symfonyResponse = new Response('Content'); $psrResponse = $psr7Factory->createResponse($symfonyResponse); This is how we can convert HttpFoundation objects to objects implementing HTTP message interfaces defined by the PSR-7 https://inviqa.com/blog/introduction-psr-7-symfony
  65. PSR-7 middlewares AccessLog AttributeMapper AuraRouter AuraSession BasePath BasicAuthentication BlockSpam Cache

    ClientIp Cors Csp Csrf https://github.com/oscarotero/psr7-middlewares DebugBar Delay DetectDevice DigestAuthentication EncodingNegotiator ErrorHandler Expires FastRoute FormTimestamp Firewall FormatNegotiator Geolocate GoogleAnalytics Honeypot Https ImageTransformer IncludeResponse JsonSchema LanguageNegotiation LeagueRoute MethodOverride Minify Payload PhpSession Piwik ReadResponse Recaptcha Rename ResponseTime Robots SaveResponse Shutdown TrailingSlash Uuid Whoops Www Following PSR-7 Standard allows you to use middlewares
  66. Serializer Getting representations of our objects, back and forth Serializer

    HttpFoundation Validator Form Guard
  67. Serializer - The idea $recipe = new Recipe(); $recipe->setName($content['name']); $recipe->setEnergy($content['energy']);

    $recipe->setServings($content['servings']); ... The request -> Our object (manual deserialization)
  68. Serializer - The idea $recipe = new Recipe(); $recipe->setName($content['name']); $recipe->setEnergy($content['energy']);

    $recipe->setServings($content['servings']); ... ... $responseData = [ 'id' => $recipe->getId(), 'name' => $recipe->getName(), 'energy' => $recipe->getEnergy(), 'servings' => $recipe->getServings(), ]; $response = new JsonResponse($responseData, 201); The request -> Our object (manual deserialization) Our object -> The response (manual serialization)
  69. Serializer - The idea $recipe = new Recipe(); $recipe->setName($content['name']); $recipe->setEnergy($content['energy']);

    $recipe->setServings($content['servings']); ... ... $responseData = [ 'id' => $recipe->getId(), 'name' => $recipe->getName(), 'energy' => $recipe->getEnergy(), 'servings' => $recipe->getServings(), ]; $response = new JsonResponse($responseData, 201); A lot of repetitive, boring work! The request -> Our object (manual deserialization) Our object -> The response (manual serialization)
  70. Serializing $responseData = [ 'id' => $recipe->getId(), 'name' => $recipe->getName(),

    'energy' => $recipe->getEnergy(), 'servings' => $recipe->getServings(), ]; $response = new JsonResponse($responseData, 201); Converting our object into a JSON response
  71. Serializing $responseData = [ 'id' => $recipe->getId(), 'name' => $recipe->getName(),

    'energy' => $recipe->getEnergy(), 'servings' => $recipe->getServings(), ]; $response = new JsonResponse($responseData, 201); -------------- $response = new Response($serializer->serialize($recipe, 'json'), 201); Converting our object into a JSON response
  72. Deserializing $recipe = new Recipe(); $recipe->setName($content['name']); $recipe->setEnergy($content['energy']); $recipe->setServings($content['servings']); Converting the

    JSON content request into an object
  73. Deserializing $recipe = new Recipe(); $recipe->setName($content['name']); $recipe->setEnergy($content['energy']); $recipe->setServings($content['servings']); ------------------ $recipe

    = $serializer->deserialize($content, Recipe::class, 'json'); Converting the JSON content request into an object
  74. Serializer - The idea Turns objects into a specific format,

    and the other way around
  75. Setting up the Serializer use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Encoder\JsonEncoder;

    use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; $encoders = array(new XmlEncoder(), new JsonEncoder()); $normalizers = array(new ObjectNormalizer()); serializer = new Serializer($normalizers, $encoders);
  76. Setting up the Serializer use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Encoder\JsonEncoder;

    use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; $encoders = array(new XmlEncoder(), new JsonEncoder()); $normalizers = array(new ObjectNormalizer()); serializer = new Serializer($normalizers, $encoders); http://symfony.com/doc/current/components/serializer.html
  77. Representation in API != what we have in DB

  78. Representation in API != what we have in DB {

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "victoria@limenius.com", avatar: "avatar.jpg", twitter_handler: "vicqr", profile: { id: 19, bio: "My bio." } }
  79. Representation in API != what we have in DB {

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "victoria@limenius.com", avatar: "avatar.jpg", twitter_handler: "vicqr", profile: { id: 19, bio: "My bio." } } I may want to name this “username”
  80. Representation in API != what we have in DB {

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "victoria@limenius.com", avatar: "avatar.jpg", twitter_handler: "vicqr", profile: { id: 19, bio: "My bio." } } I may want to name this “username” Don’t what to expose it!
  81. Representation in API != what we have in DB {

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "victoria@limenius.com", avatar: "avatar.jpg", twitter_handler: "vicqr", profile: { id: 19, bio: "My bio." } } I may want to name this “username” Don’t what to expose it! Only in the profile, not in the list
  82. Representation in API != what we have in DB {

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "victoria@limenius.com", avatar: "avatar.jpg", twitter_handler: "vicqr", profile: { id: 19, bio: "My bio." } } I may want to name this “username” Don’t what to expose it! We want to add “thumb_” Only in the profile, not in the list
  83. Representation in API != what we have in DB {

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "victoria@limenius.com", avatar: "avatar.jpg", twitter_handler: "vicqr", profile: { id: 19, bio: "My bio." } } I may want to name this “username” Don’t what to expose it! Only in the profile, not in the list We want to add “thumb_” Only in version 2 of the API
  84. Representation in API != what we have in DB {

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "victoria@limenius.com", avatar: "avatar.jpg", twitter_handler: "vicqr", profile: { id: 19, bio: "My bio." } } I may want to name this “username” Don’t what to expose it! Only in the profile, not in the list We want to add “thumb_” Only in version 2 of the API I’d like to show it as any other field
  85. Annotations MaxDepth - Detect and limit the serialization depth -

    Especially useful when serializing large trees
  86. Annotations MaxDepth - Detect and limit the serialization depth -

    Especially useful when serializing large trees Groups - Sometimes, you want to serialize different sets of attributes from your entities - Groups are a handy way to achieve this need
  87. Let’s create a GET endpoint When I request "GET /recipes"

    Then the response status code should be 200 And only the following properties should exist: “” name servings “”
  88. Let’s create a GET endpoint class Recipe { /** *

    @Groups({"detail", "overview"}) */ public $name; /** * @Groups({"detail"}) */ public $energy; /** * @Groups({"detail", "overview"}) */ public $servings; }
  89. Let’s create a GET endpoint class Recipe { /** *

    @Groups({"detail", "overview"}) */ public $name; /** * @Groups({"detail"}) */ public $energy; /** * @Groups({"detail", "overview"}) */ public $servings; }
  90. Let’s create a GET endpoint $groups = ['groups' => ['overview']];

    $response = new Response($serializer->serialize($recipes, 'json', $groups), 201); $response->headers->set('Content-Type', 'application/json'); return $response; Now we can serialize only the group overview, if we want
  91. Creating your custom normalizers For example, to serialize attributes with

    a different name
  92. Creating your custom normalizers use Symfony\Component\Serializer\NameConverter\NameConverterInterface; class OrgPrefixNameConverter implements NameConverterInterface

    { public function normalize($propertyName) { return 'prefix_'.$propertyName; } public function denormalize($propertyName) { // remove prefix_ prefix return 'prefix_' === substr($propertyName, 0, 7) ? substr($propertyName, 7) : $propertyName; } } For example, to serialize attributes with a different name
  93. Validator Enforcing sanity Validator HttpFoundation Serializer Form Guard

  94. Validator - The idea The values sent to our DB

    must meet certain constraints We can’t leave that work to the DB itself We need a previous validation Provides tools to validate the incoming data
  95. Simplest example use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; $validator =

    Validation::createValidator(); $violations = $validator->validate('Spanish omelette', array( new Length(array('min' => 2)) ));
  96. Simplest example use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; $validator =

    Validation::createValidator(); $violations = $validator->validate('Spanish omelette', array( new Length(array('min' => 2)) )); Constraints... (rule formulation)
  97. Simplest example use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; $validator =

    Validation::createValidator(); $violations = $validator->validate('Spanish omelette', array( new Length(array('min' => 2)) )); and validators (logic there)
  98. Simplest example use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; $validator =

    Validation::createValidator(); $violations = $validator->validate('Spanish omelette', array( new Length(array('min' => 2)) )); What happens with more complex validations… such as validating an object?
  99. Validating objects /** * @ORM\Column(type="integer") * @Assert\NotBlank() * @Assert\GreaterThanOrEqual( *

    value=0, * message="This universe is not so lucky. This value must be at least zero" * ) **/ private $energy; /** * @ORM\Column(type="integer") * @Assert\NotBlank() **/ private $servings; Validator needs to know which constraints apply to the object properties
  100. Validating objects /** * @ORM\Column(type="integer") * @Assert\NotBlank() * @Assert\GreaterThanOrEqual( *

    value=0, * message="This universe is not so lucky. This value must be at least zero" * ) **/ private $energy; /** * @ORM\Column(type="integer") * @Assert\NotBlank() **/ private $servings; Annotations are handy, but it can be done with yml, xml, explicit PHP...
  101. Constraints https://symfony.com/doc/current/reference/constraints.html You have about 50 constraints defined in the

    Validator component (from NotNull to ISBN…)
  102. Constraints https://symfony.com/doc/current/reference/constraints.html You have about 50 constraints defined in the

    Validator component (from NotNull to ISBN…) And you can create your own
  103. Returning errors in our API if (0 !== count($violations)) {

    $errors = []; foreach ($violations as $violation) { $errors[$violation->getPropertyPath()] = $violation->getMessage(); } $response = new JsonResponse($errors, 400); $response->send(); return; } Good validation and error handling are key in an API
  104. Form Powering up validation and deserialization Form HttpFoundation Serializer Validator

    Guard
  105. Form - The idea Not much difference between handling an

    HTML form and API data Equivalent to deserializing + validating A powerful deserializer We can reuse work done in HTML forms Provides powerful validation plus serialization
  106. Form - The idea Not much difference between handling an

    HTML form and API data Equivalent to deserializing + validating A powerful deserializer We can reuse work done in HTML forms https://knpuniversity.com/screencast/symfony-rest/form-post
  107. Let’s create a PUT endpoint When I request "PUT /recipes/1"

    Then the response status code should be 200
  108. Let’s create a PUT endpoint $formFactory = Forms::createFormFactoryBuilder() ->addExtension(new ValidatorExtension($validator))

    ->getFormFactory(); $data = json_decode($request->getContent(), true); $recipe = $this->getDoctrine()->getRepository('AppBundle:Recipe')->find($recipeId); $form = $formFactory->create(RecipeType::class, $recipe); $form->submit($data); if (!$form->isValid()) { $response = new Response($serializer->serialize($form, 'json'), 400); $response->headers->set('Content-Type', 'application/json'); return $response; }
  109. Let’s create a PUT endpoint $formFactory = Forms::createFormFactoryBuilder() ->addExtension(new ValidatorExtension($validator))

    ->getFormFactory(); $data = json_decode($request->getContent(), true); $recipe = $this->getDoctrine()->getRepository('AppBundle:Recipe')->find($recipeId); $form = $formFactory->create(RecipeType::class, $recipe); $form->submit($data); if (!$form->isValid()) { $response = new Response($serializer->serialize($form, 'json'), 400); $response->headers->set('Content-Type', 'application/json'); return $response; }
  110. Let’s create a PUT endpoint $formFactory = Forms::createFormFactoryBuilder() ->addExtension(new ValidatorExtension($validator))

    ->getFormFactory(); $data = json_decode($request->getContent(), true); $recipe = $this->getDoctrine()->getRepository('AppBundle:Recipe')->find($recipeId); $form = $formFactory->create(RecipeType::class, $recipe); $form->submit($data); if (!$form->isValid()) { $response = new Response($serializer->serialize($form, 'json'), 400); $response->headers->set('Content-Type', 'application/json'); return $response; }
  111. Let’s create a PUT endpoint $formFactory = Forms::createFormFactoryBuilder() ->addExtension(new ValidatorExtension($validator))

    ->getFormFactory(); $data = json_decode($request->getContent(), true); $recipe = $this->getDoctrine()->getRepository('AppBundle:Recipe')->find($recipeId); $form = $formFactory->create(RecipeType::class, $recipe); $form->submit($data); if (!$form->isValid()) { $response = new Response($serializer->serialize($form, 'json'), 400); $response->headers->set('Content-Type', 'application/json'); return $response; }
  112. Let’s create a PUT endpoint $formFactory = Forms::createFormFactoryBuilder() ->addExtension(new ValidatorExtension($validator))

    ->getFormFactory(); $data = json_decode($request->getContent(), true); $recipe = $this->getDoctrine()->getRepository('AppBundle:Recipe')->find($recipeId); $form = $formFactory->create(RecipeType::class, $recipe); $form->submit($data); if (!$form->isValid()) { $response = new Response($serializer->serialize($form, 'json'), 400); $response->headers->set('Content-Type', 'application/json'); return $response; }
  113. Let’s create a PUT endpoint $formFactory = Forms::createFormFactoryBuilder() ->addExtension(new ValidatorExtension($validator))

    ->getFormFactory(); $data = json_decode($request->getContent(), true); $recipe = $this->getDoctrine()->getRepository('AppBundle:Recipe')->find($recipeId); $form = $formFactory->create(RecipeType::class, $recipe); $form->submit($data); if (!$form->isValid()) { $response = new Response($serializer->serialize($form, 'json'), 400); $response->headers->set('Content-Type', 'application/json'); return $response; } https://github.com/VictoriaQ/rest-symfony-components/commit/bd2e044d6a5269e4e415d8fadcf7710a9ede27de
  114. POST to create and PUT to update?

  115. On PUT and POST Common knowledge: - POST to create

    resources - PUT to update resources
  116. On PUT and POST Common knowledge: - POST to create

    resources - PUT to update resources Not true
  117. On PUT and POST Common knowledge: - POST to create

    resources - PUT to update resources Not true (but somehow true)
  118. On PUT and POST Common knowledge: - POST to create

    resources - PUT to update resources Not true (but somehow true) (some people get very angry with this)
  119. On PUT and POST PUT if: - The operation is

    idempotent - URI = address of the resource BOTH need to be true. Otherwise, POST. This is the whole truth
  120. And do I have to return the resource?

  121. Do I have to return the resource? Many say that

    we do not Some clients assume that we will
  122. Do I have to return the resource? Many say that

    we do not Some clients assume that we will As always, be consistent with your choices
  123. Guard Authenticating Guard HttpFoundation Serializer Validator Form

  124. Guard - The idea Security Component allows to implement authentication

    Very powerful and flexible… But complex too Simplifies the authentication provided by the Security Component
  125. Just about one class with seven methods We only need

    to implement GuardAuthenticationInterface
  126. Just about one class with seven methods We only need

    to implement GuardAuthenticationInterface With its seven methods: class TokenAuthenticator extends AbstractGuardAuthenticator implements Guard\GuardAuthenticatorInterface { public function getCredentials(Request $request) public function getUser($credentials, UserProviderInterface $userProvider) public function checkCredentials($credentials, UserInterface $user) public function onAuthenticationFailure(Request $request, AuthenticationException $exception) public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) public function supportsRememberMe() public function start(Request $request, AuthenticationException $authException = null) } http://symfony.com/blog/new-in-symfony-2-8-guard-authentication-component
  127. Just about one class with seven methods We only need

    to implement GuardAuthenticationInterface With its seven methods: class TokenAuthenticator extends AbstractGuardAuthenticator implements Guard\GuardAuthenticatorInterface { public function getCredentials(Request $request) public function getUser($credentials, UserProviderInterface $userProvider) public function checkCredentials($credentials, UserInterface $user) public function onAuthenticationFailure(Request $request, AuthenticationException $exception) public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) public function supportsRememberMe() public function start(Request $request, AuthenticationException $authException = null) } http://symfony.com/blog/new-in-symfony-2-8-guard-authentication-component
  128. But at this point You need EventDispatcher, HttpKernel, Routing...

  129. But at this point You need EventDispatcher, HttpKernel, Routing... Starts

    looking like a good idea to use a complete framework
  130. III. USING THE FULL FRAMEWORK

  131. Uses all these Components http://symfony.com/projects/symfonyfs (actually a few more)

  132. Components and bundles - Component -> decoupled and reusable library

    - Bundle -> tied to the Symfony Framework
  133. Components and bundles - Component -> decoupled and reusable library

    - Bundle -> tied to the Symfony Framework Often you have: Functionality Library Integration with SF Configuration Dependency injection Bundle +
  134. Components and bundles - Component -> decoupled and reusable library

    - Bundle -> tied to the Symfony Framework Often you have: Functionality Library Integration with SF Configuration Dependency injection Bundle + Let’s see a few Bundles that can be useful for our API
  135. JMSSerializerBundle

  136. JMSSerializerBundle Another possibility for serialization / deserialization - JMSSerializer has

    lots of useful annotations - Perhaps easier to setup and start using than Sf Serializer - Sf Serializer is more about writing your own normalizers
  137. JMSSerializerBundle Some cool features: - Three exclusion strategies (Exclude, Groups,

    Versions) - Configurable properties (Virtual Props., Accessors) - Events provide extra flexibility - XML highly configurable Good alternative to Serializer, up to you
  138. FOSRestBundle

  139. FOSRestBundle Set of tools: - View layer that deals with

    the different representations - Body request decoder - Listener that performs content negotiation - Parameter validator and converter - ALLOW header manager - Exception decoder - Unified REST routing system
  140. FOSRestBundle Set of tools: - View layer that deals with

    the different representations - Body request decoder - Listener that performs content negotiation - Parameter validator and converter - ALLOW header manager - Exception decoder - Unified REST routing system You can live without it, but it is quite useful when you have it
  141. LexikJWTAuthenticationBundle

  142. LexikJWTAuthenticationBundle JSON Web Token (JWT) is a compact URL-safe means

    of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).
  143. LexikJWTAuthenticationBundle JSON Web Token (JWT) is a compact URL-safe means

    of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS). Two built-in token encoders, based on: - namshi/jose - lcobucci/jwt
  144. LexikJWTAuthenticationBundle POST /login_check HTTP/1.1 Host: localhost:8000 Content-Type: application/json Accept: application/json

    Cache-Control: no-cache {"_username": "user", "_password": "userpass"} endpoint configured in security.yml credentials
  145. LexikJWTAuthenticationBundle {"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE0MzIzODc2ODcsInVzZXJuYW1l IjoidXNlciIsImlhdCI6IjE0MzIzMDEyODcifQ.QgtJ-C96gMDzl1syuRGdpIb02sGnC-oKyLFqiNMX9Lny hYV__R7itxLPEepOqzkhOiL-O7EdFEdqFjK9vRgk67MRgfKCh5Yuir9aUkVYeZXpyVOGUgPGDpHtFi74_G9 EY4FGXzt7zhHoP_Y-6GwFSc_7HTeW7sm3ifGpYSlifyx3jmggkHoTc-1_kxXFdceoSCOoPf-uDaaQtLOwKF qUJCYaAcGXaS0FI_GDnIyX4bDCcPi3_6AvLLuSmfCaZqRofPvasANfhxRmE6iZ9wZ9dcKX1siPu7F_lFpJe 3UgMy6hr3kDYPz5H4gOcu0A3JWV7lCIpThF8j-G1eN7PCIeLCQBpq08rkM9SrohWUiMcuqCQYjWIbB-jF0Z Uv762Cvv2Y- e9gqXsCYmg9NrLURLNJdce_Yl3QyWECYSKwaFkz8p8VTqrKtxrM0Gd-qNZ_uT4Hq8-BZ7ZDGCfQ0Dm4qC2P qXtPFfvmi_RQ9S2xmx93At0gBtLRrQPSqxv7ZfqtDjAKsCgOsMPU1yS-4h8s3Wxb4_flwvBZJbnHgkmFFR8

    -p_a8FwAHXoHXkPokTX-hZy-UjfV1x8N2F8u0_ndsHS0cZdFhX9grpBsRoR058YFTqFgAC-6s3d6AeAX7WX VIKu- x1qjcsrkNND2jxoEppZphT6ecC0JgT6ioGFuHYZWbA"}
  146. LexikJWTAuthenticationBundle GET /recipes/1 HTTP/1.1 Host: localhost:8000 Content-Type: application/json Accept: application/json

    Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE0MzIzNzk0MjQsInVzZXJuYW1lIjoidXNlci IsImlhdCI6IjE0MzIyOTMwMjQifQ.Wb8NlbNNvCA5V7QxKoN_uYLmCRiqjt0ES5gIlvpyrBmVKMAXQ_cj6M NqbXcHp28NXNGJidcV2qJ1-8zWGkbHBDMZCAOp_JX74EXdeQYa0c2bV7HzKyUCEX8bOii25Jmt8YpWUa5og w58rWVhE-2CRn0fQS_gHn7d3Ke1w4NPjaLgnhSthZKfR2Ytd9U6A_3w6X2fS2R24DWWmb8KThMtSwXOtGVq EQVEnsro91RPHARg-na1ww_BnE-3Xas_427g84bsUHNIIlDs7BYAEF323h3AZrPB4Tx1tNabEWIsBJzEAKE LBDoCrqmspK0AFrGx-RIjiRMpMzDlTHMjt6hML7SO1y3AJbrD5AsxNLITtnaxfkFYl3n0XIH8Gd2ZRdFDsj XGPPZ6VLFHfUIe1BoPmLtFt5CK4PeDb-_BsbdA3tGEkVgfTrh9bSMY9mXZ-KCg8F8cbK5A-CqURqRFpTts1 OBRUcOR1c6GtrPHpGsuoPGE90mTiOJPyajUK8lgdv1yBMt7WSEJNRXMLxetk57l53FLiLVObfV7D-LGC9R1 8gIedzxmTXXxteV83izNuzQChAiRBU73W-5kjGkmpvr05Q_rd33dTn9wxEe0I0nnEK_MeSvqz2nH23xj7RU wGH qsrWlfVZZqZcVP59njex0LWWgb7PGKmJwL8ze2MrBft-U Provides a very simple way of authenticating
  147. BazingaHateoasBundle

  148. BazingaHateoasBundle Allows you to reach REST level 3 Hypermedia As

    The Engine Of Application State
  149. BazingaHateoasBundle GET /recipes/1 HTTP/1.1 Host: localhost:8000 Content-Type: application/json Accept: application/json

    { "id":1, "name":"Spanish omelette", "energy":"500", "servings":4 }
  150. BazingaHateoasBundle GET /recipes/1 HTTP/1.1 Host: localhost:8000 Content-Type: application/json Accept: application/json

    { "id":1, "name":"Spanish omelette", "energy":"500", "servings":4, "_links": { "self": { "href": "\/recipes\/1" } } } Level 3! :-)
  151. BazingaHateoasBundle use Doctrine\ORM\Mapping as ORM; use Hateoas\Configuration\Annotation as Hateoas; use

    JMS\Serializer\Annotation as Serializer; /** * Recipe * * @ORM\Table() * @ORM\Entity(repositoryClass="AppBundle\Entity\RecipeRepository") * @Hateoas\Relation("self", * href = @Hateoas\Route( * "fosrest_api_get_recipe", * parameters = { "id" = "expr(object.getId())" })) */ class Recipe { … }
  152. BazingaHateoasBundle use Doctrine\ORM\Mapping as ORM; use Hateoas\Configuration\Annotation as Hateoas; use

    JMS\Serializer\Annotation as Serializer; /** * Recipe * * @ORM\Table() * @ORM\Entity(repositoryClass="AppBundle\Entity\RecipeRepository") * @Hateoas\Relation("self", * href = @Hateoas\Route( * "fosrest_api_get_recipe", * parameters = { "id" = "expr(object.getId())" })) */ class Recipe { … } Potentially, allows you to reach level 3
  153. And do I actually HAVE TO reach level 3?

  154. Do I have to reach level 3 of REST? “If

    the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period.” Roy T. Fielding (author of REST thesis and REST concept itself) http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven Theoretically:
  155. Do I have to reach level 3 of REST? “If

    the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period.” Theoretically: In the Real World: Roy T. Fielding (author of REST thesis and REST concept itself) http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
  156. Do I have to reach level 3 of REST? “If

    the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period.” Theoretically: In the Real World: It is up to you, you have to evaluate Roy T. Fielding (author of REST thesis and REST concept itself) http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
  157. NelmioAPIDocBundle

  158. NelmioAPIDocBundle Documentation bundle You create documentation while coding Uses code

    introspection a lot
  159. NelmioAPIDocBundle Documentation bundle You create documentation while coding Uses code

    introspection a lot Easy to keep the documentation updated
  160. NelmioAPIDocBundle /** * Gets a Recipe * @ApiDoc( * resource=true,

    * section="recipe", * deprecated="true", * tags={"hi", "anothertag" = "#34a523"}, * views = { "default", "v2" }, * statusCodes = { * 200 = "Returned when successful", * 404 = "Returned when the recipe is not found" * }) * @QueryParam(name="page", requirements="\d+", default="1", description="Page of the overview.") * @param ParamFetcher $paramFetcher * @return array */ public function getPlatosAction(ParamFetcher $paramFetcher) {
  161. NelmioAPIDocBundle

  162. NelmioAPIDocBundle

  163. IV. TESTING

  164. Testing, some tips Invest some time in setting up a

    nice testing environment
  165. Testing, some tips Postman, easy to set-up Invest some time

    in setting up a nice testing environment
  166. Postman

  167. Testing, some tips Postman, easy to set-up Guzzle + PHPUnit,

    your bread and butter Invest some time in setting up a nice testing environment
  168. Guzzle $data = array( 'name' => 'Spanish omelette', 'energy' =>

    500, 'servings' => 4 ); $request = $client->post('/recipes', null, json_encode($data)); $response = $request->send(); echo $response;
  169. Testing, some tips Postman, easy to set-up Guzzle + PHPUnit,

    your bread and butter Very important to handle errors properly Invest some time in setting up a nice testing environment
  170. Error handling - Nested errors Create test “errors.children.servings.errors should exist”

    { "code": 400, "message": "Validation Failed", "errors": { "children": { "name": [ ], "energy": [ ], "servings": { "errors": [ "This value should be greater than or equal to 0." ] } } }
  171. Error handling - HTML errors Not the type of error

    that we want to receive in our API
  172. Error handling - HTML errors Not the type of error

    that we want to receive in our API <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="robots" content="noindex,nofollow" /> <title> No route found for &quot;GET /recipes&quot; (404 Not Found) </title> <link href="http://dietoworld.lo/bundles/framework/css/structure.css" rel="stylesheet" /> <link href="http://dietoworld.lo/bundles/framework/css/body.css" rel="stylesheet" /> <link href="http://dietoworld.lo/bundles/framework/css/exception.css" rel="stylesheet" type="text/css" media="all" /> </head> <body> <div id="content"> <div class="header clear-fix"> <div class="header-logo"> <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALYAAAA+CAMAAACxzRGDAAAAUVBMVEX////Ly8yko6WLioxkYmVXVVkwLjLl5eWxsLJKSEzy8vJxcHLY 2Ni+vb89Oz9XVVh+fH+Yl5n///+xsbLY2Nlxb3KkpKWXlph+fX+LiYy+vr/IZP61AAAAAXRSTlMAQObYZgAABRBJREFUeNrVmtuWoyAQRS1FEEQSzQU7//+hYxUiXsKQZLJW M+chsUloN+WhCuguYoKyYqzmvGasKqH4HyRKxndipcgcumH8qViTM7TkUclcwaHmf5XM0eWq4km1KjdqXfMXJHVe1J3hL8lk5fCGv6wmT+o0d87U+XNrk0Y9nfv+7LM6 ZJH5ZBL6LAbSxQ3Q5FDr22Skr8PQSy4n7isnsQxSX4r6pobhjCHHeDNOKrO3yGmCvZOjV9jmt8ulTdXFKdbKLNh+kOMvBzuVRa4Y7MUsdEUSWQe7xxCfZmcwjHU83LqzFv SbJQOXQvptbPnEFoyZtUUGwTeKuLuTHyT1kaP0P6cR01OKvv448gtl61dqZfmJezQmU/t+1R2fJLtBwXV6uWGwB9SZPrn0fKO2WAvQN1PUhHjTom3xgXYTkvlSKHs19Ohsl ETq6X3HrXbjt8XbGj9b4Gi+lUAnL6XxQj8Pyk9N4Bt1xUrsLVN/3isYMug8rODMdbgOvoHs8uAb2fcANIAzkKCLYy+AXRpSU8sr1r4P67xhLgPp7vM32zlqt7Bhq2fI1Hwp+VgA Nxok59SsGV3oqdUL0YVDMRY7Yg8QLbVUU4NZNoOq5hJHuxEM28Sh/IyUZ8D3reR+yc58EGvOy2U0HQL6G9V+kWyEWHmzaMx6t4o9RhOm/riUiYrzqij4Ptqkn7AaCXqc+F 47m04ahfde7YIz8RHEBN6BdV
  173. Error handling - HTML errors We can use the Symfony

    Crawler to return something nicer <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="robots" content="noindex,nofollow" /> <title> No route found for &quot;GET /recipes&quot; (404 Not Found) </title> <link href="http://dietoworld.lo/bundles/framework/css/structure.css" rel="stylesheet" /> <link href="http://dietoworld.lo/bundles/framework/css/body.css" rel="stylesheet" /> <link href="http://dietoworld.lo/bundles/framework/css/exception.css" rel="stylesheet" type="text/css" media="all" /> </head> <body> <div id="content"> <div class="header clear-fix"> <div class="header-logo"> <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALYAAAA+CAMAAACxzRGDAAAAUVBMVEX////Ly8yko6WLioxkYmVXVVkwLjLl5eWxsLJKSEzy8vJxcHLY 2Ni+vb89Oz9XVVh+fH+Yl5n///+xsbLY2Nlxb3KkpKWXlph+fX+LiYy+vr/IZP61AAAAAXRSTlMAQObYZgAABRBJREFUeNrVmtuWoyAQRS1FEEQSzQU7//+hYxUiXsKQZLJW M+chsUloN+WhCuguYoKyYqzmvGasKqH4HyRKxndipcgcumH8qViTM7TkUclcwaHmf5XM0eWq4km1KjdqXfMXJHVe1J3hL8lk5fCGv6wmT+o0d87U+XNrk0Y9nfv+7LM6 ZJH5ZBL6LAbSxQ3Q5FDr22Skr8PQSy4n7isnsQxSX4r6pobhjCHHeDNOKrO3yGmCvZOjV9jmt8ulTdXFKdbKLNh+kOMvBzuVRa4Y7MUsdEUSWQe7xxCfZmcwjHU83LqzFv SbJQOXQvptbPnEFoyZtUUGwTeKuLuTHyT1kaP0P6cR01OKvv448gtl61dqZfmJezQmU/t+1R2fJLtBwXV6uWGwB9SZPrn0fKO2WAvQN1PUhHjTom3xgXYTkvlSKHs19Ohsl ETq6X3HrXbjt8XbGj9b4Gi+lUAnL6XxQj8Pyk9N4Bt1xUrsLVN/3isYMug8rODMdbgOvoHs8uAb2fcANIAzkKCLYy+AXRpSU8sr1r4P67xhLgPp7vM32zlqt7Bhq2fI1Hwp+VgA Nxok59SsGV3oqdUL0YVDMRY7Yg8QLbVUU4NZNoOq5hJHuxEM28Sh/IyUZ8D3reR+yc58EGvOy2U0HQL6G9V+kWyEWHmzaMx6t4o9RhOm/riUiYrzqij4Ptqkn7AaCXqc+F 47m04ahfde7YIz8RHEBN6BdV This is what we want
  174. Testing, some tips Postman, easy to set-up Guzzle + PHPUnit,

    your bread and butter Very important to handle errors properly Isolated DB for testing (can use SQlite) Invest some time in setting up a nice testing environment
  175. Testing, some tips Postman, easy to set-up Guzzle + PHPUnit,

    your bread and butter Very important to handle errors properly Isolated DB for testing (can use SQlite) Set up proper fixtures (Alice & Faker are nice tools) Invest some time in setting up a nice testing environment
  176. Fixtures with Alice & Faker AppBundle\Entity\Recipe: recipe1: name: Spanish omelette

    energy: 500 servings: 4 recipe{2..20}: name: <sentence(2)> energy: <numberBetween(300,600)> servings: <numberBetween(1,4)>
  177. Fixtures with Alice & Faker AppBundle\Entity\Recipe: recipe1: name: Spanish omelette

    energy: 500 servings: 4 recipe{2..20}: name: <sentence(2)> energy: <numberBetween(300,600)> servings: <numberBetween(1,4)> Known values, that we may want to use in some test
  178. Fixtures with Alice & Faker AppBundle\Entity\Recipe: recipe1: name: Spanish omelette

    energy: 500 servings: 4 recipe{2..20}: name: <sentence(2)> energy: <numberBetween(300,600)> servings: <numberBetween(1,4)> https://github.com/hautelook/AliceBundle https://github.com/fzaninotto/Faker Known values, that we may want to use in some test Values created with generators Many generators available:
  179. V. USES FOR YOUR API

  180. Uses for an API Api serves content to Mobile/Rich web

    apps/…
  181. Uses for an API Api serves content to Mobile/Rich web

    apps/… Abstract parts of a monolith into isolated services
  182. Uses for an API Api serves content to Mobile/Rich web

    apps/… Abstract parts of a monolith into isolated services Undertake a migration
  183. Migrations - Option 1

  184. Migrations - Option 1 We replace functionalities, one route at

    a time
  185. Migrations - Option 1 We replace functionalities, one route at

    a time
  186. Migrations - Option 1

  187. Migrations - Option 1 https://speakerdeck.com/hhamon/bringing-symfony-components-into-your-legacy-code

  188. Migrations - Option 1 https://speakerdeck.com/hhamon/bringing-symfony-components-into-your-legacy-code Good idea when the team

    feels very comfortable with the new framework
  189. Migrations - Option 2

  190. Migrations - Option 2 Old system replace its code with

    calls to new Symfony API
  191. Migrations - Option 2 Good idea when team knows legacy

    code very well
  192. Migrations - Option 2 Good idea when team knows legacy

    code very well
  193. VI. FINAL THOUGHTS

  194. Summarizing Whatever the choices you make building your API: -

    Be consistent - Take profit of using a common language REST and Symfony Components make that easy - They fit well together, both built around HTTP - A few Components provide a lot - You can consider using the full framework if you want more Make sure that you set up a proper test environment
  195. None
  196. https://github.com/VictoriaQ/sonatademo @vicqr victoria@limenius.com Training, consulting and development https://github.com/VictoriaQ/rest-symfony-components @vicqr victoria@limenius.com

    Thanks!