Slide 1

Slide 1 text

Using API platform to build ticketing system Antonio Perić-Mažar, Locastic 22.11.2019. - #SymfonyCon, Amsterdam

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

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

Slide 4

Slide 4 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 & API Platform Experience

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

What is API platform ?

Slide 7

Slide 7 text

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

Slide 8

Slide 8 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 9

Slide 9 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 10

Slide 10 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 11

Slide 11 text

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

Slide 12

Slide 12 text

Creating Simple CRUD in a minute

Slide 13

Slide 13 text

Create Entity Step One id; } }

Slide 14

Slide 14 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 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

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

Serialization Groups Read Write

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

Use YAML Configuration advice

Slide 25

Slide 25 text

User Management & Security

Slide 26

Slide 26 text

• Avoid using FOSUserBundle • not well suited with API • to much overhead and not needed complexity • Use Doctrine User Provider • simple and easy to integrate • bin/console maker:user User management

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 … }

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

Slide 29

Slide 29 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 30

Slide 30 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 31

Slide 31 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 32

Slide 32 text

No content

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 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 35

Slide 35 text

No content

Slide 36

Slide 36 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 37

Slide 37 text

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

Slide 38

Slide 38 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 39

Slide 39 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 40

Slide 40 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 or your custom Provider). JWT tip A database-less user provider

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Creating multi-language APIs

Slide 43

Slide 43 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 44

Slide 44 text

Slide 45

Slide 45 text

Slide 46

Slide 46 text

getTranslation()->setTitle($title); return $this; } public function getTitle() { return $this->getTranslation()->getTitle(); } }

Slide 47

Slide 47 text

title = $title; return $this; } public function getTitle() { return $this->title; } }

Slide 48

Slide 48 text

title = $title; return $this; } public function getTitle() { return $this->title; } } AppBundle\Entity\Post: itemOperations: get: method: GET put: method: PUT normalization_context: groups: ['translations'] collectionOperations: get: method: GET post: method: POST normalization_context: groups: ['translations'] attributes: filters: ['translation.groups'] normalization_context: groups: ['post_read'] denormalization_context: groups: ['post_write']

Slide 49

Slide 49 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 50

Slide 50 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 51

Slide 51 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 52

Slide 52 text

• https://github.com/lexik/LexikTranslationBundle • or you can write your own: • https://locastic.com/blog/symfony-translation-process- automation/ Static translation

Slide 53

Slide 53 text

Manipulating context and avoding to have / api/admin

Slide 54

Slide 54 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 55

Slide 55 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 56

Slide 56 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 57

Slide 57 text

Symfony Messenger Component

Slide 58

Slide 58 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 59

Slide 59 text

No content

Slide 60

Slide 60 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 61

Slide 61 text

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

Slide 62

Slide 62 text

CQRS Symfony Messenger & API Platform

Slide 63

Slide 63 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 64

Slide 64 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 65

Slide 65 text

No content

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

• If same Symfony application is dispatching and consuming messages it works very well • If different Symfony applications are consuming messages, there are issues • If Symfony applications consume some 3-rd party non-Symfony messages, there is huge issue Symfony & Messenger

Slide 68

Slide 68 text

{ "body":"{\"key\":\"TESTING\",\"variables\":{\"UserName\":\"some test username \", \"Greetings\":\"Some test greeting 4faf188d396e99e023a3f77c04a9c06d\",\"Email\": \"[email protected]\"},\"to\":\"[email protected]\"}", "properties":[ ], "headers":{ "type":"App\\Message\\CommunicationMessage", "X-Message-Stamp-Symfony\\Component\\Messenger\\Stamp\ \BusNameStamp":"[{\"busName\":\"command_bus\"}]", "X-Message-Stamp-Symfony\\Component\\Messenger\\Stamp\ \SentStamp":"[{\"senderClass\":\"Enqueue\\\\MessengerAdapter\\\\QueueInteropTransport\", \"senderAlias\":\"communication_sync_amqp\"}]", "Content-Type":"application\/json" } }

Slide 69

Slide 69 text

{ "body":"{\"key\":\"TESTING\",\"variables\":{\"UserName\":\"some test username \", \"Greetings\":\"Some test greeting 4faf188d396e99e023a3f77c04a9c06d\",\"Email\": \"[email protected]\"},\"to\":\"[email protected]\"}", "properties":[ ], "headers":{ "type":"App\\Message\\CommunicationMessage", "X-Message-Stamp-Symfony\\Component\\Messenger\\Stamp\ \BusNameStamp":"[{\"busName\":\"command_bus\"}]", "X-Message-Stamp-Symfony\\Component\\Messenger\\Stamp\ \SentStamp":"[{\"senderClass\":\"Enqueue\\\\MessengerAdapter\\\\QueueInteropTransport\", \"senderAlias\":\"communication_sync_amqp\"}]", "Content-Type":"application\/json" } }

Slide 70

Slide 70 text

{ "body":"{\"key\":\"TESTING\",\"variables\":{\"UserName\":\"some test username \", \"Greetings\":\"Some test greeting 4faf188d396e99e023a3f77c04a9c06d\",\"Email\": \"[email protected]\"},\"to\":\"[email protected]\"}", "properties":[ ], "headers":{ "type":"App\\Message\\CommunicationMessage", "X-Message-Stamp-Symfony\\Component\\Messenger\\Stamp\ \BusNameStamp":"[{\"busName\":\"command_bus\"}]", "X-Message-Stamp-Symfony\\Component\\Messenger\\Stamp\ \SentStamp":"[{\"senderClass\":\"Enqueue\\\\MessengerAdapter\\\\QueueInteropTransport\", \"senderAlias\":\"communication_sync_amqp\"}]", "Content-Type":"application\/json" } } String contains escaped JSON

Slide 71

Slide 71 text

{ "key": "TESTING", "variables": { "username": "some test userName", "Grettings": "Some test greeting 4faf188d396e99e023a3f77c04a9c06d", "Email:" "[email protected]" } }

Slide 72

Slide 72 text

namespace App\Messenger; … class ExternalJsonMessageSerializer implements SerializerInterface { public function decode(array $encodedEnvelope): Envelope { $body = $encodedEnvelope['variables']; $headers = $encodedEnvelope['key']; $data = json_decode($body, true); if (null === $data) { throw new MessageDecodingFailedException('Invalid JSON'); } if (!isset($headers['type'])) { throw new MessageDecodingFailedException('Missing "type" header'); } if ($headers !== 'TESTING') { throw new MessageDecodingFailedException(sprintf('Invalid type "%s"', $headers)); } // in case of redelivery, unserialize any stamps $stamps = []; if (isset($headers['stamps'])) { $stamps = unserialize($headers['stamps']); } $envelope = $this->createMyExternalMessage($data); // in case of redelivery, unserialize any stamps $stamps = []; if (isset($headers['stamps'])) { $stamps = unserialize($headers['stamps']); } $envelope = $envelope->with(... $stamps); return $envelope; } … }

Slide 73

Slide 73 text

namespace App\Messenger; … class ExternalJsonMessageSerializer implements SerializerInterface { public function decode(array $encodedEnvelope): Envelope { $body = $encodedEnvelope['variables']; $headers = $encodedEnvelope['key']; $data = json_decode($body, true); if (null === $data) { throw new MessageDecodingFailedException('Invalid JSON'); } if (!isset($headers['type'])) { throw new MessageDecodingFailedException('Missing "type" header'); } if ($headers !== 'TESTING') { throw new MessageDecodingFailedException(sprintf('Invalid type "%s"', $headers)); } // in case of redelivery, unserialize any stamps $stamps = []; if (isset($headers['stamps'])) { $stamps = unserialize($headers['stamps']); } $envelope = $this->createMyExternalMessage($data); // in case of redelivery, unserialize any stamps $stamps = []; if (isset($headers['stamps'])) { $stamps = unserialize($headers['stamps']); } $envelope = $envelope->with(... $stamps); return $envelope; } … } private function createMyExternalMessage($data): Envelope { $message = new MyExternalMessage($data); $envelope = new Envelope($message); // needed only if you need this to be sent through the non-default bus $envelope = $envelope->with(new BusNameStamp('command.bus')); return $envelope; }

Slide 74

Slide 74 text

namespace App\Messenger; … class ExternalJsonMessageSerializer implements SerializerInterface { public function decode(array $encodedEnvelope): Envelope { $body = $encodedEnvelope['variables']; $headers = $encodedEnvelope['key']; $data = json_decode($body, true); if (null === $data) { throw new MessageDecodingFailedException('Invalid JSON'); } if (!isset($headers['type'])) { throw new MessageDecodingFailedException('Missing "type" header'); } if ($headers !== 'TESTING') { throw new MessageDecodingFailedException(sprintf('Invalid type "%s"', $headers)); } // in case of redelivery, unserialize any stamps $stamps = []; if (isset($headers['stamps'])) { $stamps = unserialize($headers['stamps']); } $envelope = $this->createMyExternalMessage($data); // in case of redelivery, unserialize any stamps $stamps = []; if (isset($headers['stamps'])) { $stamps = unserialize($headers['stamps']); } $envelope = $envelope->with(... $stamps); return $envelope; } … } private function createMyExternalMessage($data): Envelope { $message = new MyExternalMessage($data); $envelope = new Envelope($message); // needed only if you need this to be sent through the non-default bus $envelope = $envelope->with(new BusNameStamp('command.bus')); return $envelope; } framework: messenger: ... transports: ... # a transport used consuming messages from an external system # messages are not meant to be *sent* to this transport external_messages: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' serializer: App\Messenger\ExternalJsonMessageSerializer options: # assume some other system will create this auto_setup: false # exchange config not needed because that's only # for *sending* messages queues: messages_from_external: ~ ...

Slide 75

Slide 75 text

• https://github.com/Happyr/message-serializer (custom serializer) • http://developer.happyr.com/symfony-messenger-on-aws-lambda • have in mind that Messenger component is similar to ORM • will work in most of cases but in same situation you will need to do custom/your own solution More help:

Slide 76

Slide 76 text

Handling emails

Slide 77

Slide 77 text

• Symfony Mailer component • load balancer • /w messenger component can do async in easy way • high availability / failover • Amazon SES, Gmail, Mailchim Mandril, Mailgun, Postmark, Sendgrid Handling emails

Slide 78

Slide 78 text

Creating reports (exports)

Slide 79

Slide 79 text

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

Slide 80

Slide 80 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 81

Slide 81 text

Slide 82

Slide 82 text

setOrderNumber($data->getOrderNumber()); if ($data->getStatus() instanceof Status) { $export->setOrderStatus($data->getStatus()->getStatus()); } $export->setOrderDate($data->getOrderDate()); … … $export->setTotalNumberOfTicketsInOrder($data->getTickets()->count()); $export->setTicketRevenue($data->getTicketsRevenue()->getFormatted()); $export->setDeliveryStatus($data->getDeliveryStatus()->getStatus()); $export->setDeliveryMethodRevenue($data->getDeliveryCost()->getFormatted()); $export->setFeeRevenue($data->getTotalFees()->getFormatted()); $export->setTicketTypes($data->getTicketTypesFormatted()); return $export; } /** * {@inheritdoc} */ public function supportsTransformation($data, string $to, array $context = []): bool { return OrderExport::class === $to && $data instanceof Order; } }

Slide 83

Slide 83 text

Real-time applications with API platform

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 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.rocks

Slide 88

Slide 88 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 89

Slide 89 text

Testing

Slide 90

Slide 90 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 91

Slide 91 text

Handy testing tools

Slide 92

Slide 92 text

• Created for PHPUnit • Manipulates the Symfony HttpKernel directly to simulate HTTP requests and responses • Huge performance boost compared to triggering real network requests • Also good to consider Sylius/ApiTestCase The test Http Client in API Platform

Slide 93

Slide 93 text

No content

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

Faker (/w Alice) Library for generating random data

Slide 97

Slide 97 text

Postman tests Newman + Postman

Slide 98

Slide 98 text

Postman tests + Newman Newman + Postman

Slide 99

Slide 99 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 100

Slide 100 text

No content

Slide 101

Slide 101 text

Api Platform (Symfony) is awesome! Conclusion

Slide 102

Slide 102 text

Thank you!

Slide 103

Slide 103 text

Questions? Antonio Perić-Mažar t: @antonioperic m: [email protected]