Slide 1

Slide 1 text

Using API platform to build ticketing system Antonio Perić-Mažar, Locastic Paula Čučuk, Locastic 18.10.2019. - #sfday

Slide 2

Slide 2 text

Antonio Perić-Mažar CEO @ Locastic Co-founder @ Litto Co-founder @ Tinel Meetup t: @antonioperic m: [email protected]

Slide 3

Slide 3 text

Paula Čučuk Lead Backend Developer @ Locastic Partner @ Locastic t: @paulala_14 m: [email protected]

Slide 4

Slide 4 text

Locastic Helping clients create web and mobile apps since 2011 • UX/UI • Mobile apps • Web apps • Training & Consulting www.locastic.com @locastic

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

• API Platform & Symfony • Ticketing platform: GFNY (franchise business) • ~ year and half in production • ~ 60 000 tickets released & race results stored in DB • ~ 20 000 users/racers, • ~ 60 users with admin roles • 48 events in 26 countries, users from 82 countries • 8 different languages including Hebrew and Indonesian Context & our Experience

Slide 7

Slide 7 text

• Social network • chat based • matching similar to Tinder :) • few CRM/ERP applications Context & our Experience

Slide 8

Slide 8 text

What is API platform ?

Slide 9

Slide 9 text

–Fabien Potencier (creator of Symfony), SymfonyCon 2017 “API Platform is the most advanced API platform, in any framework or language.”

Slide 10

Slide 10 text

• full stack framework dedicated to API-Driven projects • contains a PHP library to create a fully featured APIs supporting industry standards (JSON-LD, Hydra, GraphQL, OpenAPI…) • provides ambitious Javascript tooling to consume APIs in a snap • Symfony official API stack (instead of FOSRestBundle) • shipped with Docker and Kubernetes integration API Platform

Slide 11

Slide 11 text

• creating, retrieving, updating and deleting (CRUD) resources • data validation • pagination • filtering • sorting • hypermedia/HATEOAS and content negotiation support (JSON-LD and Hydra, JSON:API, HAL…) API Platform built-in features:

Slide 12

Slide 12 text

• GraphQL support • Nice UI and machine-readable documentations (Swagger UI/ OpenAPI, GraphiQL…) • authentication (Basic HTP, cookies as well as JWT and OAuth through extensions) • CORS headers • security checks and headers (tested against OWASP recommendations) API Platform built-in features:

Slide 13

Slide 13 text

• invalidation-based HTTP caching • and basically everything needed to build modern APIs. API Platform built-in features:

Slide 14

Slide 14 text

Creating Simple CRUD in a minute

Slide 15

Slide 15 text

Create Entity Step One id; } }

Slide 16

Slide 16 text

Create Mapping Step Two # config/doctrine/Greeting.orm.yml App\Entity\Greeting: type: entity table: greeting id: id: type: integer generator: { strategy: AUTO } fields: name: type: string length: 100

Slide 17

Slide 17 text

Add Validation Step Three # config/validator/greeting.yaml App\Entity\Greeting: properties: name: - NotBlank: ~

Slide 18

Slide 18 text

Expose Resource Step Four # config/api_platform/resources.yaml resources: App\Entity\Greeting: ~

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

Serialization Groups

Slide 24

Slide 24 text

User Management & Security

Slide 25

Slide 25 text

• Avoid using FOSUserBundle • not well suited with API • Use Doctrine User Provider • simple and easy to integrate User management

Slide 26

Slide 26 text

// src/Entity/User.php namespace App\Entity; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Serializer\Annotation\Groups; class User implements UserInterface { /** * @Groups({"user-read"}) */ private $id; /** * @Groups({"user-write", "user-read"}) */ private $email; /** * @Groups({"user-read"}) */ private $roles = []; /** * @Groups({"user-write"}) */ private $plainPassword; private $password; … getters and setters … }

Slide 27

Slide 27 text

// src/Entity/User.php namespace App\Entity; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Serializer\Annotation\Groups; class User implements UserInterface { /** * @Groups({"user-read"}) */ private $id; /** * @Groups({"user-write", "user-read"}) */ private $email; /** * @Groups({"user-read"}) */ private $roles = []; /** * @Groups({"user-write"}) */ private $plainPassword; private $password; … getters and setters … } # config/doctrine/User.orm.yml App\Entity\User: type: entity table: users repositoryClass: App\Repository\UserRepository id: id: type: integer generator: { strategy: AUTO } fields: email: type: string length: 255 password: type: string length: 255 roles: type: array

Slide 28

Slide 28 text

// src/Entity/User.php namespace App\Entity; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Serializer\Annotation\Groups; class User implements UserInterface { /** * @Groups({"user-read"}) */ private $id; /** * @Groups({"user-write", "user-read"}) */ private $email; /** * @Groups({"user-read"}) */ private $roles = []; /** * @Groups({"user-write"}) */ private $plainPassword; private $password; … getters and setters … } # config/doctrine/User.orm.yml App\Entity\User: type: entity table: users repositoryClass: App\Repository\UserRepository id: id: type: integer generator: { strategy: AUTO } fields: email: type: string length: 255 password: type: string length: 255 roles: type: array # config/api_platform/resources.yaml resources: App\Entity\User: attributes: normalization_context: groups: ['user-read'] denormalization_context: groups: ['user-write']

Slide 29

Slide 29 text

use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface; use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; class UserDataPersister implements ContextAwareDataPersisterInterface { private $entityManager; private $userPasswordEncoder; public function __construct(EntityManagerInterface $entityManager, UserPasswordEncoderInterface $userPasswordEncoder) { $this->entityManager = $entityManager; $this->userPasswordEncoder = $userPasswordEncoder; } public function supports($data, array $context = []): bool { return $data instanceof User; } public function persist($data, array $context = []) { /** @var User $data */ if ($data->getPlainPassword()) { $data->setPassword( $this->userPasswordEncoder->encodePassword($data, $data->getPlainPassword()) ); $data->eraseCredentials(); } $this->entityManager->persist($data); $this->entityManager->flush($data); return $data; } public function remove($data, array $context = []) { $this->entityManager->remove($data); $this->entityManager->flush(); }

Slide 30

Slide 30 text

• Lightweight and simple authentication system • Stateless: token signed and verified server-side then stored client- side and sent with each request in an Authorization header • Store the token in the browser local storage JSON Web Token (JWT)

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

• API Platform allows to easily add a JWT-based authentication to your API using LexikJWTAuthenticationBundle. • Maybe you want to use a refresh token to renew your JWT. In this case you can check JWTRefreshTokenBundle. User authentication

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

User security checker Security isDeleted()) { throw new AccountDeletedException(); } } public function checkPostAuth(UserInterface $user) { if (!$user instanceof AppUser) { return; } // user account is expired, the user may be notified if ($user->isExpired()) { throw new AccountExpiredException('...'); } } }

Slide 36

Slide 36 text

User security checker Security # config/packages/security.yaml # ... security: firewalls: main: pattern: ^/ user_checker: App\Security\UserChecker # ...

Slide 37

Slide 37 text

Resource and operation level Security # api/config/api_platform/resources.yaml App\Entity\Book: attributes: security: 'is_granted("ROLE_USER")' collectionOperations: get: ~ post: security: 'is_granted("ROLE_ADMIN")' itemOperations: get: ~ put: security_: 'is_granted("ROLE_ADMIN") or object.owner == user'

Slide 38

Slide 38 text

Resource and operation level using Voters Security # api/config/api_platform/resources.yaml App\Entity\Book: itemOperations: get: security_: 'is_granted('READ', object)' put: security_: 'is_granted('UPDATE', object)'

Slide 39

Slide 39 text

• A JWT is self-contained, meaning that we can trust into its payload for processing the authentication. In a nutshell, there should be no need for loading the user from the database when authenticating a JWT Token, the database should be hit only once for delivering the token. • It means you will have to fetch the User entity from the database yourself as needed (probably through the Doctrine EntityManager). JWT tip A database-less user provider

Slide 40

Slide 40 text

JWT tip A database-less user provider # config/packages/security.yaml security: providers: jwt: lexik_jwt: ~ security: firewalls: api: provider: jwt guard: # ...

Slide 41

Slide 41 text

Creating multi-language APIs

Slide 42

Slide 42 text

• Locastic Api Translation Bundle • Translation bundle for ApiPlatform based on Sylius translation • It requires two entities: Translatable & Translation entity • Open source • https://github.com/Locastic/ApiPlatformTranslationBundle • https://locastic.com/blog/having-troubles-with-implementing- translations-in-apiplatform/ Creating multi-language APIs

Slide 43

Slide 43 text

POST translation example Multi-language API { "datetime":"2017-10-10", "translations": { "en":{ "title":"test", "content":"test", "locale":"en" }, "de":{ "title":"test de", "content":"test de", "locale":"de" } } }

Slide 44

Slide 44 text

Get response by locale GET /api/posts/1?locale=en { "@context": "/api/v1/contexts/Post", "@id": "/api/v1/posts/1')", "@type": "Post", "id": 1, "datetime":"2019-10-10", "title":"Hello world", "content":"Hello from Verona!" }

Slide 45

Slide 45 text

Get response with all translations GET /api/posts/1?groups[]=translations { "@context": "/api/v1/contexts/Post", "@id": "/api/v1/posts/1')", "@type": "Post", "id": 1, "datetime":"2019-10-10", "translations": { "en":{ "title":"Hello world", "content":"Hello from Verona!", "locale":"en" }, "it":{ "title":"Ciao mondo", "content":"Ciao da Verona!", "locale":"it" } } }

Slide 46

Slide 46 text

• Endpoint for creating new language • Creates all Symfony translation files when new language is added • Endpoint for editing each language translation files Adding languages and translations dynamically

Slide 47

Slide 47 text

Manipulating the Context Context namespace App\Entity; use ApiPlatform\Core\Annotation\ApiResource; use Symfony\Component\Serializer\Annotation\Groups; /** * @ApiResource( * normalizationContext={"groups"={"book:output"}}, * denormalizationContext={"groups"={"book:input"}} * ) */ class Book { // ... /** * This field can be managed only by an admin * * @var bool * * @Groups({"book:output", "admin:input"}) */ public $active = false; /** * This field can be managed by any user * * @var string * * @Groups({"book:output", "book:input"}) */ public $name; // ... }

Slide 48

Slide 48 text

Manipulating the Context Context # api/config/services.yaml services: # ... 'App\Serializer\BookContextBuilder': decorates: 'api_platform.serializer.context_builder' arguments: [ '@App\Serializer\BookContextBuilder.inner' ] autoconfigure: false

Slide 49

Slide 49 text

// api/src/Serializer/BookContextBuilder.php namespace App\Serializer; use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use App\Entity\Book; final class BookContextBuilder implements SerializerContextBuilderInterface { private $decorated; private $authorizationChecker; public function __construct(SerializerContextBuilderInterface $decorated, AuthorizationCheckerInterface $authorizationChecker) { $this->decorated = $decorated; $this->authorizationChecker = $authorizationChecker; } public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array { $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes); $resourceClass = $context['resource_class'] ?? null; if ($resourceClass === Book::class && isset($context['groups']) && $this->authorizationChecker- >isGranted('ROLE_ADMIN') && false === $normalization) { $context['groups'][] = 'admin:input'; } return $context; } }

Slide 50

Slide 50 text

Symfony Messanger Component

Slide 51

Slide 51 text

• The Messenger component helps applications send and receive messages to/from other applications or via message queues. • Easy to implement • Making async easy • Many transports are supported to dispatch messages to async consumers, including RabbitMQ, Apache Kafka, Amazon SQS and Google Pub/Sub. Symfony Messenger

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

• Allows to implement the Command Query Responsibility Segregation (CQRS) pattern in a convenient way. • It also makes it easy to send messages through the web API that will be consumed asynchronously. • Async import, export, image processing… any heavy work Symfony Messenger & API Platform

Slide 55

Slide 55 text

CQRS Symfony Messenger & API Platform App\Entity\PasswordResetRequest: collectionOperations: post: status: 202 itemOperations: [] attributes: messenger: true output: false

Slide 56

Slide 56 text

CQRS Symfony Messenger & API Platform

Slide 57

Slide 57 text

CQRS /w DTO Symfony Messenger & API Platform App\Entity\User: collectionOperations: post: status: 202 itemOperations: [] attributes: messenger: “input” input: “ResetPasswordRequest::class” output: false // api/src/Entity/User.php namespace App\Entity; use ApiPlatform\Core\Annotation\ApiResource; use App\Dto\ResetPasswordRequest; final class User { }

Slide 58

Slide 58 text

CQRS /w DTO Symfony Messenger & API Platform // api/src/Handler/ResetPasswordRequestHandler.php namespace App\Handler; use App\Dto\ResetPasswordRequest; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; final class ResetPasswordRequestHandler implements MessageHandle { public function __invoke(ResetPasswordRequest $forgotPasswor { // do something with the resource } } // api/src/Dto/ResetPasswordRequest.php namespace App\Dto; use Symfony\Component\Validator\Constraints as Assert; final class ResetPasswordRequest { public $var; }

Slide 59

Slide 59 text

entityManager = $entityManager; $this->messageBus = $messageBus; } public function supports($data, array $context = []): bool { return $data instanceof ImageMedia; } public function persist($data, array $context = []) { $this->entityManager->persist($data); $this->entityManager->flush($data); $this->messageBus->dispatch(new ProcessImageMessage($data->getId())); return $data; } public function remove($data, array $context = []) { $this->entityManager->remove($data); $this->entityManager->flush(); $this->messageBus->dispatch(new DeleteImageMessage($data->getId())); } }

Slide 60

Slide 60 text

namespace App\EventSubscriber; use ApiPlatform\Core\EventListener\EventPriorities; use App\Entity\Book; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ViewEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Messenger\MessageBusInterface; final class BookMailSubscriber implements EventSubscriberInterface { private $messageBus; public function __construct(MessageBusInterface $messageBus) { $this->messageBus = $messageBus; } public static function getSubscribedEvents() { return [ KernelEvents::VIEW => ['sendMail', EventPriorities::POST_WRITE], ]; } public function sendMail(ViewEvent $event) { $book = $event->getControllerResult(); $method = $event->getRequest()->getMethod(); if (!$book instanceof Book || Request::METHOD_POST !== $method) { return; } // send to all users 2M that new book has arrived this->messageBus->dispatch(new SendEmailMessage(‘new-book’, $book->getTitle())); } }

Slide 61

Slide 61 text

• problem: • different objects from source and in our database • multiple sources of data (3rd party) • DataTransform transforms from source object to our object • exporting to CSV files Using DTOs with import and export

Slide 62

Slide 62 text

resources: App\Entity\Order: collectionOperations: get: ~ exports: method: POST path: '/orders/export' formats: csv: ['text/csv'] pagination_enabled: false output: "OrderExport::class" normalization_context: groups: ['order-export']

Slide 63

Slide 63 text

Real-time applications with API platform

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

• Redis + NodeJS • Pusher • ReactPHP • … • but to be honest PHP is not build for realtime :) Real-time applications with API platform

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

• Fast, written in Go • native browser support, no lib nor SDK required (built on top of HTTP and server-sent events) • compatible with all existing servers, even those who don't support persistent connections (serverless architecture, PHP, FastCGI…) • Automatic HTTP/2 and HTTPS (using Let's Encrypt) support • CORS support, CSRF protection mechanism • Cloud Native, follows the Twelve-Factor App methodology • Open source (AGPL) • … Mercure

Slide 68

Slide 68 text

resources: App\Entity\Greeting: attributes: mercure: true const eventSource = new EventSource('http://localhost:3000/hub?topic=' + encodeURIComponent('http://example.com/greeting/1')); eventSource.onmessage = event => { // Will be called every time an update is published by the server console.log(JSON.parse(event.data)); }

Slide 69

Slide 69 text

Testing

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

• Unit tests • test your logic, refactor your code using SOLID priciples • Integration tests • validation • 3rd party integrations • database queries • Functional tests • response code, header and content (expected fields in expected format) Type of tests

Slide 72

Slide 72 text

• Ask yourself: “Am I sure the code I tested works as it should?” • 100% coverage doesn’t guarantee your code is fully tested and working • Write test first is just one of the approaches • Legacy code: • Start replicating bugs with tests before fixing them • Test at least most important and critical parts Testing tips and tricks

Slide 73

Slide 73 text

Handy testing tools

Slide 74

Slide 74 text

PHP Matcher Library that enables you to check your response against patterns.

Slide 75

Slide 75 text

Faker Library for generating random data

Slide 76

Slide 76 text

Postman tests Newman + Postman

Slide 77

Slide 77 text

• Infection - tool for mutation testing • PHPStan - focuses on finding errors in your code without actually running it • Continuous integration (CI) -  enables you to run your tests on git on each commit Tools for checking test quality

Slide 78

Slide 78 text

Api Platform is awesome! Conclusion

Slide 79

Slide 79 text

Thank you!

Slide 80

Slide 80 text

Questions? Antonio Perić-Mažar t: @antonioperic m: [email protected] Paula Čučuk t: @paulala_14 m: [email protected]