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

Building a first class REST API with Symfony

C845a8857cadb4f26a7b4ab7604e413b?s=47 Michael C.
September 28, 2018

Building a first class REST API with Symfony

C845a8857cadb4f26a7b4ab7604e413b?s=128

Michael C.

September 28, 2018
Tweet

Transcript

  1. BUILDING FIRST-CLASS REST APIS WITH SYMFONY @MICHAELCULLUMUK SYMFONY LIVE LONDON

    2018
  2. BUILDING FIRST-CLASS REST APIS WITH SYMFONY @MICHAELCULLUMUK SYMFONY LIVE LONDON

    2018
  3. @MICHAELCULLUMUK ME?

  4. MICHAEL CULLUM @MICHAELCULLUMUK

  5. @MICHAELCULLUMUK APIS

  6. @MICHAELCULLUMUK WHAT IS REST?

  7. @MICHAELCULLUMUK REPRESENTATIONAL STATE TRANSFER

  8. @MICHAELCULLUMUK REPRESENTATIONAL STATE TRANSFER

  9. @MICHAELCULLUMUK ▸ Uniform Interface REPRESENTATIONAL STATE TRANSFER (REST)

  10. @MICHAELCULLUMUK ▸ Uniform Interface ▸ Stateless REPRESENTATIONAL STATE TRANSFER (REST)

  11. @MICHAELCULLUMUK ▸ Uniform Interface ▸ Stateless ▸ Cacheable REPRESENTATIONAL STATE

    TRANSFER (REST)
  12. @MICHAELCULLUMUK ▸ Uniform Interface ▸ Stateless ▸ Cacheable ▸ Client-Server

    REPRESENTATIONAL STATE TRANSFER (REST)
  13. @MICHAELCULLUMUK ▸ Uniform Interface ▸ Stateless ▸ Cacheable ▸ Client-Server

    ▸ Layered System REPRESENTATIONAL STATE TRANSFER (REST)
  14. @MICHAELCULLUMUK ▸ Uniform Interface ▸ Stateless ▸ Cacheable ▸ Client-Server

    ▸ Layered System ▸ Code on demand (Optional) REPRESENTATIONAL STATE TRANSFER (REST)
  15. @MICHAELCULLUMUK SO WHAT?

  16. @MICHAELCULLUMUK HTTP RESPONSE CODES

  17. @MICHAELCULLUMUK USE HTTP WELL

  18. @MICHAELCULLUMUK HTTP VERBS

  19. @MICHAELCULLUMUK THE RIGHT HTTP VERBS

  20. @MICHAELCULLUMUK HTTP RESPONSE CODES

  21. @MICHAELCULLUMUK ▸ 200 - All is right with the world

    ▸ 301 / 302 - Turn around, walk down the road and where you’re looking for is on the right ▸ 400 - You made a mistake ▸ 401 or 403 - You can’t see this, it’s a secret ▸ 404 - I can’t find what you’re asking for ▸ 405 - Fix your HTTP verbs ▸ 500 - Something is wrong with me but I don’t know what. Sorry. BASIC HTTP CODES
  22. @MICHAELCULLUMUK ▸ 201 - Created something OK ▸ 202 -

    Accepted but will be properly processed in the background ▸ 422 - Validation Error ▸ 401 - Login! ▸ 403 - You’re not supposed to be here ▸ 410 - This resource did exist, but it’s now been deleted or disabled ▸ 429 - You’ve hit a rate limit, steady on there ▸ 503 - API is not here right now, please try again later BASIC HTTP CODES
  23. @MICHAELCULLUMUK RESPONSE CODES $response->setStatusCode(422);

  24. @MICHAELCULLUMUK RESPONSE CODES $response->setStatusCode(Response::UNPROCESSABLE_ENTITY);

  25. @MICHAELCULLUMUK RESPONSE CODES <?php namespace Symfony\Component\HttpFoundation; class Response { const

    HTTP_OK = 200; const HTTP_CREATED = 201; const HTTP_ACCEPTED = 202; const HTTP_TEMPORARY_REDIRECT = 307; const HTTP_PERMANENTLY_REDIRECT = 308; const HTTP_BAD_REQUEST = 400; const HTTP_UNAUTHORIZED = 401; const HTTP_PAYMENT_REQUIRED = 402; const HTTP_FORBIDDEN = 403; const HTTP_NOT_FOUND = 404; const HTTP_METHOD_NOT_ALLOWED = 405; const HTTP_NOT_ACCEPTABLE = 406; const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407; const HTTP_REQUEST_TIMEOUT = 408; const HTTP_CONFLICT = 409; const HTTP_GONE = 410; const HTTP_LENGTH_REQUIRED = 411; const HTTP_PRECONDITION_FAILED = 412; const HTTP_REQUEST_ENTITY_TOO_LARGE = 413; const HTTP_REQUEST_URI_TOO_LONG = 414; const HTTP_UNSUPPORTED_MEDIA_TYPE = 415; const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; const HTTP_EXPECTATION_FAILED = 417; const HTTP_I_AM_A_TEAPOT = 418; const HTTP_UNPROCESSABLE_ENTITY = 422; const HTTP_TOO_MANY_REQUESTS = 429; const HTTP_INTERNAL_SERVER_ERROR = 500; const HTTP_NOT_IMPLEMENTED = 501; const HTTP_BAD_GATEWAY = 502; const HTTP_SERVICE_UNAVAILABLE = 503; const HTTP_GATEWAY_TIMEOUT = 504; }
  26. @MICHAELCULLUMUK ERRORS

  27. @MICHAELCULLUMUK AN EVENT class ExceptionListener { /** * @param GetResponseForExceptionEvent

    $event */ public function onKernelException(GetResponseForExceptionEvent $event): void { $e = $event->getException(); if ($e instanceof ValidationException) { $response = new ValidationErrorResponse($e->getErrors()); } else { $response = JsonResponse::create( [ 'code' => 'UNKNOWN_ERROR', 'message' => $e->getMessage(), ], Response::HTTP_INTERNAL_SERVER_ERROR ); } $event->setResponse($response); } }
  28. @MICHAELCULLUMUK EXCEPTION CONTROLLER OVERRIDE <?php declare(strict_types=1); namespace SamKnows\Common\Controller; class ExceptionController

    extends DefaultExceptionController { public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null): Response }
  29. @MICHAELCULLUMUK EXCEPTION CONTROLLER OVERRIDE public function showAction(Request $request, FlattenException $exception,

    DebugLoggerInterface $logger = null) { $code = $exception->getStatusCode(); $body = $this->getError($exception, $this->debug); $response = new JsonResponse( $body, $code ); $response->headers->set('Content-Type', 'application/json; charset=UTF-8'); return $response; }
  30. @MICHAELCULLUMUK EXCEPTION CONTROLLER OVERRIDE public function getError(FlattenException $exception) { $error

    = []; $mapping = $this->errorMap[$exception->getClass()]; $error['code'] = $mapping[‘code'] ?? 'UNKNOWN_ERROR', $error['message'] = $mapping[‘message'] ?? 'Unknown error'; if ($exception->getClass() === ValidationException::class) { $error['errors'] = $exception->getErrors(); } if ($this->twig->isDebug()) { $error['debug'] = sprintf( '%s in file %s amd line %d', $exception->getMessage(), $exception->getFile(), $exception->getLine() ); } return $error; } private $errorMap = [ AccessDeniedException::class => [ 'code' => 'ACCESS_DENIED', 'message' => 'Access Denied', ], ValidationException::class => [ 'code' => 'VALIDATION_FAIL', 'message' => 'Validation failed', ], ];
  31. @MICHAELCULLUMUK DTO ALL THE THINGS

  32. @MICHAELCULLUMUK PARAM CONVERTER IS YOUR FRIEND

  33. @MICHAELCULLUMUK PARAM CONVERTOR + DTO public function postAddressesAction(AddressCreateDTO $addressCreateDTO): Response

    { // ... $address = $this->entityPersister->mapAndPersist($addressCreateDTO, new Address()); // ... }
  34. @MICHAELCULLUMUK DTO PARAMETER CONVERTOR class DataTransferObjectConverter implements ParamConverterInterface { //

    Constructor etc. ... public function apply(Request $request, ParamConverter $configuration) { $data = $request->get('content'); $class = $configuration->getClass(); $dto = $this->serializer->denormalize($data, $class); if ($request->files->count() > 0) { $dto = $this->serializer->denormalize($request->files->all(), $class, [ 'object_to_populate' => $dto, ]); } $request->attributes->set($configuration->getName(), $dto); return true; } // supports() etc. ... }
  35. @MICHAELCULLUMUK VALIDATION

  36. @MICHAELCULLUMUK EXCEPTIONS!

  37. @MICHAELCULLUMUK REMOVE CONTROL FROM CONTROLLERS

  38. @MICHAELCULLUMUK CONTROLLER public function postAddressesAction(AddressCreateDTO $addressCreateDTO): Response { try {

    $address = $this->entityPersister->mapAndPersist($addressCreateDTO); } catch (ValidationException $exception) { return new ValidationErrorResponse($exception); } return $this->created( 'addresses_item', [ 'id' => $address->getId(), ] ); }
  39. @MICHAELCULLUMUK FORMATTING YOUR OUTPUT

  40. @MICHAELCULLUMUK SYMFONY SERIALIZER COMPONENT

  41. @MICHAELCULLUMUK TRANFORMERS

  42. @MICHAELCULLUMUK FRACTAL

  43. @MICHAELCULLUMUK FRACTAL TRANSFORMER class AddressTransformer extends TransformerAbstract { protected $availableIncludes

    = [ 'account', ]; public function transform(Address $address) { return [ 'id' => $address->getId(), 'address_line_1' => $address->getAddressLine1(), 'address_line_2' => $address->getAddressLine2(), 'address_line_3' => $address->getAddressLine3(), 'county' => $address->getCounty(), 'city' => $address->getCity(), 'postcode' => $address->getPostcode(), 'country' => $address->getCountry(), 'updated_at' => $address->getUpdatedAt() ? $address->getUpdatedAt()->getTimestamp() : null, ]; } }
  44. @MICHAELCULLUMUK FRACTAL TRANSFORMER // Create your resource, or collection of

    resources $resource = new Fractal\Resource\Item($book, new BookTransformer); $resource = new Fractal\Resource\Collection($books, new BookTransformer); // Turn that into a structured array (handy for XML views or auto-YAML converting) $array = $fractal->createData($resource)->toArray(); // Turn all of that into a JSON string $fractal->createData($resource)->toJson();
  45. @MICHAELCULLUMUK PAGINATION AND LINKS

  46. @MICHAELCULLUMUK NOBODY LIKES PAGINATION

  47. @MICHAELCULLUMUK ▸ Do it ▸ Link to the next and

    previous pages ▸ Detail the current page number ▸ Detail the total number of pages REST PAGINATION TODO LIST
  48. @MICHAELCULLUMUK DISABLING A TOTAL CAN MAKE THINGS QUICKER

  49. @MICHAELCULLUMUK DOCTRINE PAGINATOR

  50. @MICHAELCULLUMUK PAGERFANTA

  51. @MICHAELCULLUMUK FRACTAL INTEGRATION

  52. @MICHAELCULLUMUK PAGINATED OR NOT public function getUsersAction(Request $request, UserFilters $filters):

    Response { // ... $paginator = $this->grantRepository->findAllPaginated($filters); return $this->success($paginator, UserTransformer::class, $request->query->get('include')); } public function getUsersAction(Request $request, GrantFilters $filters): Response { // ... $users = $this->grantRepository->findAll($filters); return $this->success($users, UserTransformer::class, $request->query->get('include')); }
  53. @MICHAELCULLUMUK GETTING IT TO YOUR REPOSITORY

  54. @MICHAELCULLUMUK PUT IT IN AN OBJECT public function getUsersAction(Request $request,

    UserFilters $filters): Response { // ... $paginator = $this->grantRepository->findAllPaginated($filters); return $this->success($paginator, UserTransformer::class, $request->query->get('include')); }
  55. @MICHAELCULLUMUK SORTS AND FILTERS

  56. @MICHAELCULLUMUK THERE’S AN OBJECT FOR THAT

  57. @MICHAELCULLUMUK QUERY STRING [GET] /users?page=1&per_page=20&sorts=-id&include=groups%2Caddress&email=michael Pagination Sorting Includes Filters

  58. @MICHAELCULLUMUK CONTROLLER public function getAddressesAction(Request $request, AddressFilters $filters): Response

  59. @MICHAELCULLUMUK FILTER CLASS class AddressFilters { use PaginationFilterTrait; use SortingFiltersTrait;

    public function getUser() { return $this->user; } /** * @param int $user */ public function setUser($user) { $this->user = $user; } } trait PaginationFilterTrait { private $page = 1; private $perPage = 10; // ... Getters and setters... } trait SortingFiltersTrait { private $sorts = []; // ... Getters and setters ... }
  60. @MICHAELCULLUMUK REPOSITORY public function findAllPaginated(AddressFilters $addressFilters) { $queryBuilder = $this->createQueryBuilder('address');

    $this->addFilter($queryBuilder, 'user', $addressFilters->getUser()); $this->addSorts($queryBuilder, $filter->getSorts()); return $this->createPaginator($queryBuilder, $addressFilters->getPage(), $addressFilters->getPerPage()); }
  61. @MICHAELCULLUMUK ADD SORTS AND ADD FILTERS METHODS protected function addSorts(QueryBuilder

    $queryBuilder, array $sorts): void { if (count($sorts) > 0) { foreach ($sorts as $sort) { $field = $this->getFieldFromMap($sort[0]); if ($field) { $queryBuilder->addOrderBy($field, $sort[1]); } } } } protected function addFilter(QueryBuilder $queryBuilder, $name, $value, string $parameter = null): void { $field = $this->getFieldFromMap($name); if ($value === null || $value == '') { return; } if ($parameter === null) { $parameter = $name; } if ($field) { $queryBuilder->andWhere(sprintf('%s = :%s', $field, $parameter)); $queryBuilder->setParameter($parameter, $value); } }
  62. @MICHAELCULLUMUK TOOLS

  63. @MICHAELCULLUMUK API PLATFORM

  64. @MICHAELCULLUMUK API PLATFORM composer require api

  65. @MICHAELCULLUMUK DEMO

  66. @MICHAELCULLUMUK FOS REST BUNDLE

  67. @MICHAELCULLUMUK EXCEPTION CONTROLLER fos_rest: exception: codes: 'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404 'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT

    messages: 'Acme\HelloBundle\Exception\MyExceptionWithASafeMessage': true
  68. @MICHAELCULLUMUK VIEW LAYER public function getUsersAction() { $data = ...;

    // get data, in this case list of users. $view = $this->view($data, 200) ->setTemplate("MyBundle:Users:getUsers.html.twig") ->setTemplateVar('users') ; return $this->handleView($view); }
  69. @MICHAELCULLUMUK AUTOMATIC ROUTING # routes.yml users: type: rest host: m.example.com

    resource: Acme\Controller\UsersController <?php namespace AppBundle\Controller; class UsersController { public function getUsersAction() {} // "get_users" [GET] /users public function postUsersAction() {} // "post_users" [POST] /users public function putUserAction($slug) {} // "put_user" [PUT] /users/{slug} public function patchUserAction($slug) {} // "patch_user" [PATCH] /users/{slug} public function deleteUserAction($slug) {} // "delete_user" [DELETE] /users/{slug} }
  70. @MICHAELCULLUMUK RECAP

  71. @MICHAELCULLUMUK ▸ Principles of REST RECAP

  72. @MICHAELCULLUMUK ▸ Principles of REST ▸ Using HTTP well RECAP

  73. @MICHAELCULLUMUK ▸ Principles of REST ▸ Using HTTP well ▸

    Error handling RECAP
  74. @MICHAELCULLUMUK ▸ Principles of REST ▸ Using HTTP well ▸

    DTOs and the Param Converter RECAP
  75. @MICHAELCULLUMUK ▸ Principles of REST ▸ Using HTTP well ▸

    DTOs and the Param Converter ▸ Validation RECAP
  76. @MICHAELCULLUMUK ▸ Principles of REST ▸ Using HTTP well ▸

    DTOs and the Param Converter ▸ Validation ▸ Formatting output RECAP
  77. @MICHAELCULLUMUK ▸ Principles of REST ▸ Using HTTP well ▸

    DTOs and the Param Converter ▸ Validation ▸ Formatting output ▸ Pagination RECAP
  78. @MICHAELCULLUMUK ▸ Principles of REST ▸ Using HTTP well ▸

    DTOs and the Param Converter ▸ Validation ▸ Formatting output ▸ Pagination RECAP ▸ Sorts & Filters
  79. @MICHAELCULLUMUK ▸ Principles of REST ▸ Using HTTP well ▸

    DTOs and the Param Converter ▸ Validation ▸ Formatting output ▸ Pagination RECAP ▸ Sorts & Filters ▸ Tools ▸ API Platform ▸ FOS Rest Bundle
  80. THANKS @MICHAELCULLUMUK

  81. @MICHAELCULLUMUK ANY QUESTIONS?

  82. BUILDING FIRST-CLASS REST APIS WITH SYMFONY @MICHAELCULLUMUK SYMFONY LIVE LONDON

    2018