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

Building a first class REST API with Symfony

Michael C.
September 28, 2018

Building a first class REST API with Symfony

Michael C.

September 28, 2018
Tweet

More Decks by Michael C.

Other Decks in Programming

Transcript

  1. @MICHAELCULLUMUK ▸ Uniform Interface ▸ Stateless ▸ Cacheable ▸ Client-Server

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

    ▸ Layered System ▸ Code on demand (Optional) REPRESENTATIONAL STATE TRANSFER (REST)
  3. @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
  4. @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
  5. @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; }
  6. @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); } }
  7. @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 }
  8. @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; }
  9. @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', ], ];
  10. @MICHAELCULLUMUK PARAM CONVERTOR + DTO public function postAddressesAction(AddressCreateDTO $addressCreateDTO): Response

    { // ... $address = $this->entityPersister->mapAndPersist($addressCreateDTO, new Address()); // ... }
  11. @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. ... }
  12. @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(), ] ); }
  13. @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, ]; } }
  14. @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();
  15. @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
  16. @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')); }
  17. @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')); }
  18. @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 ... }
  19. @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()); }
  20. @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); } }
  21. @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); }
  22. @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} }
  23. @MICHAELCULLUMUK ▸ Principles of REST ▸ Using HTTP well ▸

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

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

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

    DTOs and the Param Converter ▸ Validation ▸ Formatting output ▸ Pagination RECAP ▸ Sorts & Filters
  27. @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