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

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.

Victoria Quirante

February 16, 2017
Tweet

More Decks by Victoria Quirante

Other Decks in Programming

Transcript

  1. 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 [email protected]
  2. Possible reasons AGAINST Perhaps I can think of a better

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

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

    of HTTP It means to have a common language These are very powerful reasons
  5. Sane way to approach REST 1. Learn the stuff that

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

    is commonly accepted Use nouns for the resources! Use HTTP verbs!
  7. 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!
  8. Sane way to approach REST 1. Learn the stuff that

    is commonly accepted 2. Be aware of the grey areas / controversial points
  9. 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?
  10. 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?
  11. 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?
  12. 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?
  13. 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?
  14. 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…
  15. 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
  16. 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
  17. 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
  18. 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
  19. Symfony is two things 1. A full stack framework 2.

    A set of independent components
  20. 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
  21. Where/when can you apply this knowledge - Working with the

    full Symfony framework - Using some of these components in some other framework
  22. 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
  23. 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
  24. HttpFoundation - The idea In PHP: - Request represented by

    global variables - Response generated by some functions Defines an object-oriented layer for the HTTP specification
  25. 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
  26. HttpFoundation - Request() Holds information about the client request use

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

    global variables use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals();
  28. 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 );
  29. 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') );
  30. Let’s create a POST endpoint When I request "POST /recipes"

    Then the response status code should be 201
  31. 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');
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. Serializer - The idea $recipe = new Recipe(); $recipe->setName($content['name']); $recipe->setEnergy($content['energy']);

    $recipe->setServings($content['servings']); ... The request -> Our object (manual deserialization)
  39. 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)
  40. 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)
  41. 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
  42. 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
  43. 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);
  44. 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
  45. Representation in API != what we have in DB {

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "[email protected]", avatar: "avatar.jpg", twitter_handler: "vicqr", profile: { id: 19, bio: "My bio." } }
  46. Representation in API != what we have in DB {

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

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "[email protected]", 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!
  48. Representation in API != what we have in DB {

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "[email protected]", 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
  49. Representation in API != what we have in DB {

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "[email protected]", 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
  50. Representation in API != what we have in DB {

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "[email protected]", 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
  51. Representation in API != what we have in DB {

    id: 9, name: "victoriaq", password: "encryptedPassword", email: "[email protected]", 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
  52. Annotations MaxDepth - Detect and limit the serialization depth -

    Especially useful when serializing large trees
  53. 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
  54. 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 “”
  55. Let’s create a GET endpoint class Recipe { /** *

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

    @Groups({"detail", "overview"}) */ public $name; /** * @Groups({"detail"}) */ public $energy; /** * @Groups({"detail", "overview"}) */ public $servings; }
  57. 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
  58. 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
  59. 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
  60. 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)) ));
  61. 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)
  62. 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)
  63. 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?
  64. 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
  65. 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...
  66. 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
  67. 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
  68. 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
  69. Let’s create a PUT endpoint When I request "PUT /recipes/1"

    Then the response status code should be 200
  70. 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; }
  71. 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; }
  72. 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; }
  73. 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; }
  74. 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; }
  75. 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
  76. On PUT and POST Common knowledge: - POST to create

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

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

    resources - PUT to update resources Not true (but somehow true)
  79. 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)
  80. 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
  81. Do I have to return the resource? Many say that

    we do not Some clients assume that we will
  82. 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
  83. Guard - The idea Security Component allows to implement authentication

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

    to implement GuardAuthenticationInterface
  85. 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
  86. 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
  87. But at this point You need EventDispatcher, HttpKernel, Routing... Starts

    looking like a good idea to use a complete framework
  88. 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 +
  89. 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
  90. 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
  91. 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
  92. 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
  93. 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
  94. 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).
  95. 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
  96. 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
  97. 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
  98. 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! :-)
  99. 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 { … }
  100. 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
  101. 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:
  102. 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
  103. 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
  104. NelmioAPIDocBundle Documentation bundle You create documentation while coding Uses code

    introspection a lot Easy to keep the documentation updated
  105. 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) {
  106. Testing, some tips Postman, easy to set-up Invest some time

    in setting up a nice testing environment
  107. Testing, some tips Postman, easy to set-up Guzzle + PHPUnit,

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

    500, 'servings' => 4 ); $request = $client->post('/recipes', null, json_encode($data)); $response = $request->send(); echo $response;
  109. 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
  110. 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." ] } } }
  111. Error handling - HTML errors Not the type of error

    that we want to receive in our API
  112. 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
  113. 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
  114. 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
  115. 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
  116. 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)>
  117. 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
  118. 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:
  119. Uses for an API Api serves content to Mobile/Rich web

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

    apps/… Abstract parts of a monolith into isolated services Undertake a migration
  121. 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