Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

@MICHAELCULLUMUK ME?

Slide 4

Slide 4 text

MICHAEL CULLUM @MICHAELCULLUMUK

Slide 5

Slide 5 text

@MICHAELCULLUMUK APIS

Slide 6

Slide 6 text

@MICHAELCULLUMUK WHAT IS REST?

Slide 7

Slide 7 text

@MICHAELCULLUMUK REPRESENTATIONAL STATE TRANSFER

Slide 8

Slide 8 text

@MICHAELCULLUMUK REPRESENTATIONAL STATE TRANSFER

Slide 9

Slide 9 text

@MICHAELCULLUMUK ▸ Uniform Interface REPRESENTATIONAL STATE TRANSFER (REST)

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

@MICHAELCULLUMUK ▸ Uniform Interface ▸ Stateless ▸ Cacheable ▸ Client-Server ▸ Layered System ▸ Code on demand (Optional) REPRESENTATIONAL STATE TRANSFER (REST)

Slide 15

Slide 15 text

@MICHAELCULLUMUK SO WHAT?

Slide 16

Slide 16 text

@MICHAELCULLUMUK HTTP RESPONSE CODES

Slide 17

Slide 17 text

@MICHAELCULLUMUK USE HTTP WELL

Slide 18

Slide 18 text

@MICHAELCULLUMUK HTTP VERBS

Slide 19

Slide 19 text

@MICHAELCULLUMUK THE RIGHT HTTP VERBS

Slide 20

Slide 20 text

@MICHAELCULLUMUK HTTP RESPONSE CODES

Slide 21

Slide 21 text

@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

Slide 22

Slide 22 text

@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

Slide 23

Slide 23 text

@MICHAELCULLUMUK RESPONSE CODES $response->setStatusCode(422);

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

@MICHAELCULLUMUK RESPONSE CODES

Slide 26

Slide 26 text

@MICHAELCULLUMUK ERRORS

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

@MICHAELCULLUMUK EXCEPTION CONTROLLER OVERRIDE

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

@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', ], ];

Slide 31

Slide 31 text

@MICHAELCULLUMUK DTO ALL THE THINGS

Slide 32

Slide 32 text

@MICHAELCULLUMUK PARAM CONVERTER IS YOUR FRIEND

Slide 33

Slide 33 text

@MICHAELCULLUMUK PARAM CONVERTOR + DTO public function postAddressesAction(AddressCreateDTO $addressCreateDTO): Response { // ... $address = $this->entityPersister->mapAndPersist($addressCreateDTO, new Address()); // ... }

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

@MICHAELCULLUMUK VALIDATION

Slide 36

Slide 36 text

@MICHAELCULLUMUK EXCEPTIONS!

Slide 37

Slide 37 text

@MICHAELCULLUMUK REMOVE CONTROL FROM CONTROLLERS

Slide 38

Slide 38 text

@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(), ] ); }

Slide 39

Slide 39 text

@MICHAELCULLUMUK FORMATTING YOUR OUTPUT

Slide 40

Slide 40 text

@MICHAELCULLUMUK SYMFONY SERIALIZER COMPONENT

Slide 41

Slide 41 text

@MICHAELCULLUMUK TRANFORMERS

Slide 42

Slide 42 text

@MICHAELCULLUMUK FRACTAL

Slide 43

Slide 43 text

@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, ]; } }

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

@MICHAELCULLUMUK PAGINATION AND LINKS

Slide 46

Slide 46 text

@MICHAELCULLUMUK NOBODY LIKES PAGINATION

Slide 47

Slide 47 text

@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

Slide 48

Slide 48 text

@MICHAELCULLUMUK DISABLING A TOTAL CAN MAKE THINGS QUICKER

Slide 49

Slide 49 text

@MICHAELCULLUMUK DOCTRINE PAGINATOR

Slide 50

Slide 50 text

@MICHAELCULLUMUK PAGERFANTA

Slide 51

Slide 51 text

@MICHAELCULLUMUK FRACTAL INTEGRATION

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

@MICHAELCULLUMUK GETTING IT TO YOUR REPOSITORY

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

@MICHAELCULLUMUK SORTS AND FILTERS

Slide 56

Slide 56 text

@MICHAELCULLUMUK THERE’S AN OBJECT FOR THAT

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

@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()); }

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

@MICHAELCULLUMUK TOOLS

Slide 63

Slide 63 text

@MICHAELCULLUMUK API PLATFORM

Slide 64

Slide 64 text

@MICHAELCULLUMUK API PLATFORM composer require api

Slide 65

Slide 65 text

@MICHAELCULLUMUK DEMO

Slide 66

Slide 66 text

@MICHAELCULLUMUK FOS REST BUNDLE

Slide 67

Slide 67 text

@MICHAELCULLUMUK EXCEPTION CONTROLLER fos_rest: exception: codes: 'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404 'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT messages: 'Acme\HelloBundle\Exception\MyExceptionWithASafeMessage': true

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

@MICHAELCULLUMUK AUTOMATIC ROUTING # routes.yml users: type: rest host: m.example.com resource: Acme\Controller\UsersController

Slide 70

Slide 70 text

@MICHAELCULLUMUK RECAP

Slide 71

Slide 71 text

@MICHAELCULLUMUK ▸ Principles of REST RECAP

Slide 72

Slide 72 text

@MICHAELCULLUMUK ▸ Principles of REST ▸ Using HTTP well RECAP

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

@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

Slide 80

Slide 80 text

THANKS @MICHAELCULLUMUK

Slide 81

Slide 81 text

@MICHAELCULLUMUK ANY QUESTIONS?

Slide 82

Slide 82 text

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