Share
Embed
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 dig into those components, showing how to successfully deal with problems such as adapting our models to the desired representations back and forth, how to validate the incoming data, or how to implement our authentication strategies. Furthermore, we will see which additional tools do we have when using the full framework. All of it from a practical approach based upon real use cases.
Victoria Quirante
May 30, 2017
Transcript
174 slides
-
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] -
Victoria Quirante @vicqr [email protected] Possible reasons AGAINST Perhaps I can
think of a better way to structure an API -
Victoria Quirante @vicqr [email protected] Possible reasons AGAINST Perhaps I can
think of a better way to structure an API REST seems very controversial and confusing in some points -
Victoria Quirante @vicqr [email protected] Main reasons to go for REST
REST makes the most of HTTP -
Victoria Quirante @vicqr [email protected] Main reasons to go for REST
REST makes the most of HTTP It means to have a common language -
Victoria Quirante @vicqr [email protected] Main reasons to go for REST
REST makes the most of HTTP It means to have a common language These are very powerful reasons -
Victoria Quirante @vicqr [email protected] Sane way to approach REST 1.
Learn the stuff that is commonly accepted -
Victoria Quirante @vicqr [email protected] Sane way to approach REST 1.
Learn the stuff that is commonly accepted Use nouns for the resources! -
Victoria Quirante @vicqr [email protected] Sane way to approach REST 1.
Learn the stuff that is commonly accepted Use nouns for the resources! Use HTTP verbs! -
Victoria Quirante @vicqr [email protected] 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! -
Victoria Quirante @vicqr [email protected] Sane way to approach REST 1.
Learn the stuff that is commonly accepted 2. Be aware of the grey areas / controversial points -
Victoria Quirante @vicqr [email protected] 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? -
Victoria Quirante @vicqr [email protected] 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? -
Victoria Quirante @vicqr [email protected] 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? -
Victoria Quirante @vicqr [email protected] 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? -
Victoria Quirante @vicqr [email protected] 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? -
Victoria Quirante @vicqr [email protected] 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… -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] Why Symfony & REST? REST ♥
HTTP Symfony ♥ HTTP -
Victoria Quirante @vicqr [email protected] Symfony is two things 1. A
full stack framework -
Victoria Quirante @vicqr [email protected] Symfony is two things 1. A
full stack framework 2. A set of independent components -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] Components that we are going to
see in detail HttpFoundation Serializer Validator Form Guard -
Victoria Quirante @vicqr [email protected] Where/when can you apply this knowledge
- Working with the full Symfony framework -
Victoria Quirante @vicqr [email protected] Where/when can you apply this knowledge
- Working with the full Symfony framework - Using some of these components in some other framework -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] HttpFoundation - The idea Defines an
object-oriented layer for the HTTP specification -
Victoria Quirante @vicqr [email protected] HttpFoundation - The idea In PHP:
- Request represented by global variables - Response generated by some functions Defines an object-oriented layer for the HTTP specification -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] HttpFoundation - Request() Holds information about
the client request use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); -
Victoria Quirante @vicqr [email protected] HttpFoundation - Request() Creates Request object
based on current PHP global variables use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); -
Victoria Quirante @vicqr [email protected] 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 ); -
Victoria Quirante @vicqr [email protected] 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') ); -
Victoria Quirante @vicqr [email protected] Let’s create a POST endpoint When
I request "POST /recipes" Then the response status code should be 201 -
Victoria Quirante @vicqr [email protected] Let’s create a POST endpoint use
Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); $content = json_decode($request->getContent(), true); -
Victoria Quirante @vicqr [email protected] 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'); -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Serializer Getting representations of our objects, back and forth Serializer
HttpFoundation Validator Form Guard -
Victoria Quirante @vicqr [email protected] Serializer - The idea $recipe =
new Recipe(); $recipe->setName($content['name']); $recipe->setEnergy($content['energy']); $recipe->setServings($content['servings']); ... The request -> Our object (manual deserialization) -
Victoria Quirante @vicqr [email protected] 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) -
Victoria Quirante @vicqr [email protected] 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) -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] Deserializing $recipe = new Recipe(); $recipe->setName($content['name']);
$recipe->setEnergy($content['energy']); $recipe->setServings($content['servings']); Converting the JSON content request into an object -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] Serializer - The idea Turns objects
into a specific format, and the other way around -
Victoria Quirante @vicqr [email protected] 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); -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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." } } -
Victoria Quirante @vicqr [email protected] 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” -
Victoria Quirante @vicqr [email protected] 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! -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] Annotations MaxDepth - Detect and limit
the serialization depth - Especially useful when serializing large trees -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 “” -
Victoria Quirante @vicqr [email protected] Let’s create a GET endpoint class
Recipe { /** * @Groups({"detail", "overview"}) */ public $name; /** * @Groups({"detail"}) */ public $energy; /** * @Groups({"detail", "overview"}) */ public $servings; } -
Victoria Quirante @vicqr [email protected] Let’s create a GET endpoint class
Recipe { /** * @Groups({"detail", "overview"}) */ public $name; /** * @Groups({"detail"}) */ public $energy; /** * @Groups({"detail", "overview"}) */ public $servings; } -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] Creating your custom normalizers For example,
to serialize attributes with a different name -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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)) )); -
Victoria Quirante @vicqr [email protected] 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) -
Victoria Quirante @vicqr [email protected] 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) -
Victoria Quirante @vicqr [email protected] 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? -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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... -
Victoria Quirante @vicqr [email protected] Constraints https://symfony.com/doc/current/reference/constraints.html You have about 50
constraints defined in the Validator component (from NotNull to ISBN…) -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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; } https://knpuniversity.com/screencast/symfony-rest2 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] Let’s create a PUT endpoint When
I request "PUT /recipes/1" Then the response status code should be 200 -
Victoria Quirante @vicqr [email protected] Let’s create a PUT endpoint $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; } -
Victoria Quirante @vicqr [email protected] Let’s create a PUT endpoint $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; } -
Victoria Quirante @vicqr [email protected] Let’s create a PUT endpoint $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; } -
Victoria Quirante @vicqr [email protected] Let’s create a PUT endpoint $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; } -
Victoria Quirante @vicqr [email protected] Let’s create a PUT endpoint $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 -
Victoria Quirante @vicqr [email protected] On PUT and POST Common knowledge:
- POST to create resources - PUT to update resources -
Victoria Quirante @vicqr [email protected] On PUT and POST Common knowledge:
- POST to create resources - PUT to update resources Not true -
Victoria Quirante @vicqr [email protected] On PUT and POST Common knowledge:
- POST to create resources - PUT to update resources Not true (but somehow true) -
Victoria Quirante @vicqr [email protected] 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) -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] Do I have to return the
resource? Many say that we do not Some clients assume that we will -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] Guard - The idea Security Component
allows to implement authentication Very powerful and flexible… But complex too Simplifies the authentication provided by the Security Component -
Victoria Quirante @vicqr [email protected] Just about one class with seven
methods We only need to implement GuardAuthenticationInterface -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] But at this point You need
EventDispatcher, HttpKernel, Routing... -
Victoria Quirante @vicqr [email protected] But at this point You need
EventDispatcher, HttpKernel, Routing... Starts looking like a good idea to use a complete framework -
Victoria Quirante @vicqr [email protected] Components and bundles - Component ->
decoupled and reusable library - Bundle -> tied to the Symfony Framework -
Victoria Quirante @vicqr [email protected] 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 + -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] FOSRestBundle Set of tools: - View
layer that deals with the different representations - Body request decoder - Listener that performs content negotiation - Parameter validator and converter - Exception decoder - Unified REST routing system -
Victoria Quirante @vicqr [email protected] FOSRestBundle Set of tools: - View
layer that deals with the different representations - Body request decoder - Listener that performs content negotiation - Parameter validator and converter - Exception decoder - Unified REST routing system You can live without it, but it is quite useful when you have it -
Victoria Quirante @vicqr [email protected] 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). -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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"} -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] BazingaHateoasBundle Allows you to reach REST
level 3 Hypermedia As The Engine Of Application State -
Victoria Quirante @vicqr [email protected] 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 } -
Victoria Quirante @vicqr [email protected] 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! :-) -
Victoria Quirante @vicqr [email protected] 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 { … } -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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: -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] 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 -
Victoria Quirante @vicqr [email protected] NelmioAPIDocBundle Documentation bundle You create documentation
while coding It uses code introspection a lot It comes with Swagger support -
Victoria Quirante @vicqr [email protected] NelmioAPIDocBundle Documentation bundle You create documentation
while coding It uses code introspection a lot It comes with Swagger support Easy to keep the documentation updated -
Victoria Quirante @vicqr [email protected] 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) { -
API Platform Victoria Quirante @vicqr [email protected] Built on top of
SF full framework You can build an API in 40 seconds Integrated with Swagger UI -
API Platform Victoria Quirante @vicqr [email protected] $ composer create-project api-platform/api-platform
bookshop-api $ cd bookshop-api $ php bin/console doctrine:database:create $ php bin/console doctrine:schema:create $ php bin/console server:run -
Symfony 4 & Symfony Flex It will be released in
November 2017 It moves towards zero configuration time It makes as easy to create projects with lots or few dependencies Victoria Quirante @vicqr [email protected] -
Symfony 4 & Symfony Flex It will be released in
November 2017 It moves towards zero configuration time It makes as easy to create projects with lots or few dependencies Very easily you can have a SF project with SF Flex and API Platform Victoria Quirante @vicqr [email protected] https://medium.com/@fabpot/symfony-4-a-small-screencast-cf6511823f -
Victoria Quirante @vicqr [email protected] Final thoughts (i) Whatever the choices
you make building your API: Be consistent Take profit of using a common language -
Victoria Quirante @vicqr [email protected] Final thoughts (ii) 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 -
Victoria Quirante @vicqr [email protected] Final thoughts (iii) Symfony 4 &
Symfony Flex are going to make extremely easy to create an API project