Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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]

Slide 3

Slide 3 text

I. INTRODUCTION

Slide 4

Slide 4 text

Why would I want my API to be RESTful?

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

Possible reasons AGAINST Perhaps I can think of a better way to structure an API REST seems very controversial and confusing in some points

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Main reasons to go for REST REST makes the most of HTTP It means to have a common language These are very powerful reasons

Slide 10

Slide 10 text

What you already know about REST

Slide 11

Slide 11 text

Sane way to approach REST

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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!

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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?

Slide 18

Slide 18 text

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?

Slide 19

Slide 19 text

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?

Slide 20

Slide 20 text

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?

Slide 21

Slide 21 text

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?

Slide 22

Slide 22 text

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…

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

Why Symfony & REST?

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Why Symfony & REST? REST ♥ HTTP Symfony ♥ HTTP

Slide 31

Slide 31 text

Why Symfony Components?

Slide 32

Slide 32 text

Symfony is two things 1. A full stack framework

Slide 33

Slide 33 text

Symfony is two things 1. A full stack framework 2. A set of independent components

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Components used in popular projects http://symfony.com/projects

Slide 36

Slide 36 text

Doctrine http://symfony.com/projects

Slide 37

Slide 37 text

Propel http://symfony.com/projects

Slide 38

Slide 38 text

Silex http://symfony.com/projects

Slide 39

Slide 39 text

Drupal http://symfony.com/projects

Slide 40

Slide 40 text

Laravel http://symfony.com/projects

Slide 41

Slide 41 text

Components that we are going to see in detail HttpFoundation Serializer Validator Form Guard

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

See repository https://github.com/VictoriaQ/rest-symfony-components

Slide 47

Slide 47 text

II. SYMFONY COMPONENTS

Slide 48

Slide 48 text

HttpFoundation Moving to an Object-Oriented approach HttpFoundation Serializer Validator Form Guard

Slide 49

Slide 49 text

HttpFoundation - The idea Defines an object-oriented layer for the HTTP specification

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

HttpFoundation - Request() Holds information about the client request use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals();

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Let’s create a POST endpoint When I request "POST /recipes" Then the response status code should be 201

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

And what about PSR-7?

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

Serializer Getting representations of our objects, back and forth Serializer HttpFoundation Validator Form Guard

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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)

Slide 69

Slide 69 text

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)

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

Serializer - The idea Turns objects into a specific format, and the other way around

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

Representation in API != what we have in DB

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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”

Slide 80

Slide 80 text

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!

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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 “”

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

Creating your custom normalizers For example, to serialize attributes with a different name

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

Validator Enforcing sanity Validator HttpFoundation Serializer Form Guard

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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)

Slide 97

Slide 97 text

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)

Slide 98

Slide 98 text

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?

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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...

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

Form Powering up validation and deserialization Form HttpFoundation Serializer Validator Guard

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

Let’s create a PUT endpoint When I request "PUT /recipes/1" Then the response status code should be 200

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

POST to create and PUT to update?

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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)

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

And do I have to return the resource?

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

Guard Authenticating Guard HttpFoundation Serializer Validator Form

Slide 124

Slide 124 text

Guard - The idea Security Component allows to implement authentication Very powerful and flexible… But complex too Simplifies the authentication provided by the Security Component

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

But at this point You need EventDispatcher, HttpKernel, Routing... Starts looking like a good idea to use a complete framework

Slide 130

Slide 130 text

III. USING THE FULL FRAMEWORK

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

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 +

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

JMSSerializerBundle

Slide 136

Slide 136 text

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

Slide 137

Slide 137 text

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

Slide 138

Slide 138 text

FOSRestBundle

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

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

Slide 141

Slide 141 text

LexikJWTAuthenticationBundle

Slide 142

Slide 142 text

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).

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

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

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

BazingaHateoasBundle

Slide 148

Slide 148 text

BazingaHateoasBundle Allows you to reach REST level 3 Hypermedia As The Engine Of Application State

Slide 149

Slide 149 text

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 }

Slide 150

Slide 150 text

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! :-)

Slide 151

Slide 151 text

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 { … }

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

And do I actually HAVE TO reach level 3?

Slide 154

Slide 154 text

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:

Slide 155

Slide 155 text

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

Slide 156

Slide 156 text

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

Slide 157

Slide 157 text

NelmioAPIDocBundle

Slide 158

Slide 158 text

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

Slide 159

Slide 159 text

NelmioAPIDocBundle Documentation bundle You create documentation while coding Uses code introspection a lot Easy to keep the documentation updated

Slide 160

Slide 160 text

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) {

Slide 161

Slide 161 text

NelmioAPIDocBundle

Slide 162

Slide 162 text

NelmioAPIDocBundle

Slide 163

Slide 163 text

IV. TESTING

Slide 164

Slide 164 text

Testing, some tips Invest some time in setting up a nice testing environment

Slide 165

Slide 165 text

Testing, some tips Postman, easy to set-up Invest some time in setting up a nice testing environment

Slide 166

Slide 166 text

Postman

Slide 167

Slide 167 text

Testing, some tips Postman, easy to set-up Guzzle + PHPUnit, your bread and butter Invest some time in setting up a nice testing environment

Slide 168

Slide 168 text

Guzzle $data = array( 'name' => 'Spanish omelette', 'energy' => 500, 'servings' => 4 ); $request = $client->post('/recipes', null, json_encode($data)); $response = $request->send(); echo $response;

Slide 169

Slide 169 text

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

Slide 170

Slide 170 text

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

Slide 171

Slide 171 text

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

Slide 172

Slide 172 text

Error handling - HTML errors Not the type of error that we want to receive in our API No route found for "GET /recipes" (404 Not Found)

Slide 173

Slide 173 text

Error handling - HTML errors We can use the Symfony Crawler to return something nicer No route found for "GET /recipes" (404 Not Found)

Slide 174

Slide 174 text

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

Slide 175

Slide 175 text

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

Slide 176

Slide 176 text

Fixtures with Alice & Faker AppBundle\Entity\Recipe: recipe1: name: Spanish omelette energy: 500 servings: 4 recipe{2..20}: name: energy: servings:

Slide 177

Slide 177 text

Fixtures with Alice & Faker AppBundle\Entity\Recipe: recipe1: name: Spanish omelette energy: 500 servings: 4 recipe{2..20}: name: energy: servings: Known values, that we may want to use in some test

Slide 178

Slide 178 text

Fixtures with Alice & Faker AppBundle\Entity\Recipe: recipe1: name: Spanish omelette energy: 500 servings: 4 recipe{2..20}: name: energy: servings: 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:

Slide 179

Slide 179 text

V. USES FOR YOUR API

Slide 180

Slide 180 text

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

Slide 181

Slide 181 text

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

Slide 182

Slide 182 text

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

Slide 183

Slide 183 text

Migrations - Option 1

Slide 184

Slide 184 text

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

Slide 185

Slide 185 text

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

Slide 186

Slide 186 text

Migrations - Option 1

Slide 187

Slide 187 text

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

Slide 188

Slide 188 text

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

Slide 189

Slide 189 text

Migrations - Option 2

Slide 190

Slide 190 text

Migrations - Option 2 Old system replace its code with calls to new Symfony API

Slide 191

Slide 191 text

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

Slide 192

Slide 192 text

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

Slide 193

Slide 193 text

VI. FINAL THOUGHTS

Slide 194

Slide 194 text

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

Slide 195

Slide 195 text

No content

Slide 196

Slide 196 text

https://github.com/VictoriaQ/sonatademo @vicqr [email protected] Training, consulting and development https://github.com/VictoriaQ/rest-symfony-components @vicqr [email protected] Thanks!