SlideShare une entreprise Scribd logo
1  sur  103
Télécharger pour lire hors ligne
Using API platform to build ticketing
system
Antonio Perić-Mažar, Locastic
22.11.2019. - #SymfonyCon, Amsterdam
Antonio
Perić-Mažar
CEO @ Locastic
Co-founder @ Litto
Co-founder @ Tinel Meetup
t: @antonioperic
m: antonio@locastic.com
Locastic
Helping clients create web
and mobile apps since 2011
• UX/UI
• Mobile apps
• Web apps
• Training & Consulting
www.locastic.com
@locastic
• 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
• Social network
• chat based
• matching similar to Tinder :)
• few CRM/ERP applications
Context & API Platform
Experience
What is API platform ?
–Fabien Potencier (creator of Symfony), SymfonyCon 2017
“API Platform is the most advanced API platform,
in any framework or language.”
• 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
• 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:
• 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:
• invalidation-based HTTP caching
• and basically everything needed to build modern APIs.
API Platform built-in
features:
Creating Simple CRUD
in a minute
Create
Entity
Step One
<?php
// src/Entity/Greeting.php
namespace AppEntity;
class Greeting
{
private $id;
public $name = '';
public function getId(): int
{
return $this->id;
}
}
Create
Mapping
Step Two
# config/doctrine/Greeting.orm.yml
AppEntityGreeting:
type: entity
table: greeting
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
name:
type: string
length: 100
Add
Validation
Step Three
# config/validator/greeting.yaml
AppEntityGreeting:
properties:
name:
- NotBlank: ~
Expose
Resource
Step Four # config/api_platform/resources.yaml
resources:
AppEntityGreeting: ~
Serialization Groups
Read
Write
Use
YAML
Configuration advice
User Management &
Security
• 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
// src/Entity/User.php
namespace AppEntity;
use SymfonyComponentSecurityCoreUserUserInterface;
use SymfonyComponentSerializerAnnotationGroups;
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 …
}
// src/Entity/User.php
namespace AppEntity;
use SymfonyComponentSecurityCoreUserUserInterface;
use SymfonyComponentSerializerAnnotationGroups;
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
AppEntityUser:
type: entity
table: users
repositoryClass: AppRepositoryUserRepository
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
email:
type: string
length: 255
password:
type: string
length: 255
roles:
type: array
// src/Entity/User.php
namespace AppEntity;
use SymfonyComponentSecurityCoreUserUserInterface;
use SymfonyComponentSerializerAnnotationGroups;
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
AppEntityUser:
type: entity
table: users
repositoryClass: AppRepositoryUserRepository
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:
AppEntityUser:
attributes:
normalization_context:
groups: ['user-read']
denormalization_context:
groups: ['user-write']
use ApiPlatformCoreDataPersisterContextAwareDataPersisterInterface;
use AppEntityUser;
use DoctrineORMEntityManagerInterface;
use SymfonyComponentSecurityCoreEncoderUserPasswordEncoderInterface;
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();
}
• 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)
• 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
User security
checker
Security
<?php
namespace AppSecurity;
use AppExceptionAccountDeletedException;
use AppSecurityUser as AppUser;
use SymfonyComponentSecurityCoreExceptionAccountExpiredException;
use SymfonyComponentSecurityCoreExceptionCustomUserMessageAuthenticat
use SymfonyComponentSecurityCoreUserUserCheckerInterface;
use SymfonyComponentSecurityCoreUserUserInterface;
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
// user is deleted, show a generic Account Not Found message.
if ($user->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('...');
}
}
}
User security
checker
Security
# config/packages/security.yaml
# ...
security:
firewalls:
main:
pattern: ^/
user_checker: AppSecurityUserChecker
# ...
Resource and
operation
level
Security
# api/config/api_platform/resources.yaml
AppEntityBook:
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'
Resource and
operation
level using
Voters
Security
# api/config/api_platform/resources.yaml
AppEntityBook:
itemOperations:
get:
security_: 'is_granted('READ', object)'
put:
security_: 'is_granted('UPDATE', object)'
• 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
JWT tip
A database-less user
provider
# config/packages/security.yaml
security:
providers:
jwt:
lexik_jwt: ~
security:
firewalls:
api:
provider: jwt
guard:
# ...
Creating
multi-language APIs
• 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
<?php
use LocasticApiPlatformTranslationBundleModelAbstractTranslatable;
class Post extends AbstractTranslatable
{
// ...
protected function createTranslation()
{
return new PostTranslation();
}
}
<?php
use LocasticApiPlatformTranslationBundleModelAbstractTranslatable;
class Post extends AbstractTranslatable
{
// ...
protected function createTranslation()
{
return new PostTranslation();
}
}
use LocasticApiPlatformTranslationBundleModelAbstractTranslatable;
class Post extends AbstractTranslatable
{
// ...
/**
* @Groups({"post_write", "translations"})
*/
protected $translations;
}
<?php
use LocasticApiPlatformTranslationBundleModelAbstractTranslatable;
class Post extends AbstractTranslatable
{
// ...
protected function createTranslation()
{
return new PostTranslation();
}
}
use LocasticApiPlatformTranslationBundleModelAbstractTranslatable;
class Post extends AbstractTranslatable
{
// ...
/**
* @Groups({"post_write", "translations"})
*/
protected $translations;
}
<?php
use LocasticApiPlatformTranslationBundleModelAbstractTranslatable;
use SymfonyComponentSerializerAnnotationGroups;
class Post extends AbstractTranslatable
{
// ...
/**
* @var string
*
* @Groups({"post_read"})
*/
private $title;
public function setTitle($title)
{
$this->getTranslation()->setTitle($title);
return $this;
}
public function getTitle()
{
return $this->getTranslation()->getTitle();
}
}
<?php
use SymfonyComponentSerializerAnnotationGroups;
use LocasticApiTranslationBundleModelAbstractTranslation;
class PostTranslation extends AbstractTranslation
{
// ...
/**
* @var string
*
* @Groups({"post_read", "post_write", "translations"})
*/
private $title;
/**
* @var string
* @Groups({"post_write", "translations"})
*/
protected $locale;
public function setTitle($title)
{
$this->title = $title;
return $this;
}
public function getTitle()
{
return $this->title;
}
}
<?php
use SymfonyComponentSerializerAnnotationGroups;
use LocasticApiTranslationBundleModelAbstractTranslation;
class PostTranslation extends AbstractTranslation
{
// ...
/**
* @var string
*
* @Groups({"post_read", "post_write", "translations"})
*/
private $title;
/**
* @var string
* @Groups({"post_write", "translations"})
*/
protected $locale;
public function setTitle($title)
{
$this->title = $title;
return $this;
}
public function getTitle()
{
return $this->title;
}
}
AppBundleEntityPost:
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']
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"
}
}
}
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!"
}
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"
}
}
}
• https://github.com/lexik/LexikTranslationBundle
• or you can write your own:
• https://locastic.com/blog/symfony-translation-process-
automation/
Static translation
Manipulating context
and avoding to have /
api/admin
Manipulating
the Context
Context
namespace AppEntity;
use ApiPlatformCoreAnnotationApiResource;
use SymfonyComponentSerializerAnnotationGroups;
/**
* @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;
// ...
}
Manipulating
the Context
Context
# api/config/services.yaml
services:
# ...
'AppSerializerBookContextBuilder':
decorates: 'api_platform.serializer.context_builder'
arguments: [ '@AppSerializerBookContextBuilder.inner' ]
autoconfigure: false
// api/src/Serializer/BookContextBuilder.php
namespace AppSerializer;
use ApiPlatformCoreSerializerSerializerContextBuilderInterface;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentSecurityCoreAuthorizationAuthorizationCheckerInterface;
use AppEntityBook;
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;
}
}
Symfony Messenger
Component
• 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
• 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
CQRS
Symfony Messenger & API Platform
AppEntityPasswordResetRequest:
collectionOperations:
post:
status: 202
itemOperations: []
attributes:
messenger: true
output: false
CQRS
Symfony Messenger & API Platform
<?php
namespace AppHandler;
use AppEntityPasswordResetRequest;
use SymfonyComponentMessengerHandlerMessageHandlerInterfac
final class PasswordResetRequestHandler implements MessageHand
{
public function __invoke(PasswordResetRequest $forgotPassw
{
// do some heavy things
}
}
<?php
namespace AppEntity;
final class PasswordResetRequest
{
public $email;
}
<?php
namespace AppDataPersister;
use ApiPlatformCoreDataPersisterContextAwareDataPersisterInterface;
use AppEntityImageMedia;
use DoctrineORMEntityManagerInterface;
use SymfonyComponentMessengerMessageBusInterface;
class ImageMediaDataPersister implements ContextAwareDataPersisterInterface
{
private $entityManager;
private $messageBus;
public function __construct(EntityManagerInterface $entityManager, MessageBusInterface $messageBus)
{
$this->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()));
}
}
namespace AppEventSubscriber;
use ApiPlatformCoreEventListenerEventPriorities;
use AppEntityBook;
use SymfonyComponentEventDispatcherEventSubscriberInterface;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpKernelEventViewEvent;
use SymfonyComponentHttpKernelKernelEvents;
use SymfonyComponentMessengerMessageBusInterface;
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()));
}
}
• 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
{
"body":"{"key":"TESTING","variables":{"UserName":"some test username ",
"Greetings":"Some test greeting 4faf188d396e99e023a3f77c04a9c06d","Email":
"antonio@locastic.com"},"to":"antonio@locastic.com"}",
"properties":[
],
"headers":{
"type":"AppMessageCommunicationMessage",
"X-Message-Stamp-SymfonyComponentMessengerStamp
BusNameStamp":"[{"busName":"command_bus"}]",
"X-Message-Stamp-SymfonyComponentMessengerStamp
SentStamp":"[{"senderClass":"EnqueueMessengerAdapterQueueInteropTransport",
"senderAlias":"communication_sync_amqp"}]",
"Content-Type":"application/json"
}
}
{
"body":"{"key":"TESTING","variables":{"UserName":"some test username ",
"Greetings":"Some test greeting 4faf188d396e99e023a3f77c04a9c06d","Email":
"antonio@locastic.com"},"to":"antonio@locastic.com"}",
"properties":[
],
"headers":{
"type":"AppMessageCommunicationMessage",
"X-Message-Stamp-SymfonyComponentMessengerStamp
BusNameStamp":"[{"busName":"command_bus"}]",
"X-Message-Stamp-SymfonyComponentMessengerStamp
SentStamp":"[{"senderClass":"EnqueueMessengerAdapterQueueInteropTransport",
"senderAlias":"communication_sync_amqp"}]",
"Content-Type":"application/json"
}
}
{
"body":"{"key":"TESTING","variables":{"UserName":"some test username ",
"Greetings":"Some test greeting 4faf188d396e99e023a3f77c04a9c06d","Email":
"antonio@locastic.com"},"to":"antonio@locastic.com"}",
"properties":[
],
"headers":{
"type":"AppMessageCommunicationMessage",
"X-Message-Stamp-SymfonyComponentMessengerStamp
BusNameStamp":"[{"busName":"command_bus"}]",
"X-Message-Stamp-SymfonyComponentMessengerStamp
SentStamp":"[{"senderClass":"EnqueueMessengerAdapterQueueInteropTransport",
"senderAlias":"communication_sync_amqp"}]",
"Content-Type":"application/json"
}
}
String contains escaped JSON
{
"key": "TESTING",
"variables": {
"username": "some test userName",
"Grettings": "Some test greeting 4faf188d396e99e023a3f77c04a9c06d",
"Email:" "antonio@locastic.com"
}
}
namespace AppMessenger;
…
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;
}
…
}
namespace AppMessenger;
…
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;
}
namespace AppMessenger;
…
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: AppMessengerExternalJsonMessageSerializer
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: ~
...
• 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:
Handling emails
• 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
Creating reports
(exports)
• 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
resources:
AppEntityOrder:
collectionOperations:
get: ~
exports:
method: POST
path: '/orders/export'
formats:
csv: ['text/csv']
pagination_enabled: false
output: "OrderExport::class"
normalization_context:
groups: ['order-export']
<?php
namespace AppDTO;
use SymfonyComponentSerializerAnnotationGroups;
final class OrderExport
{
/**
* @var string
* @Groups({"order-export"})
*/
private $orderStatus;
/**
* @var string
* @Groups({"order-export"})
*/
private $orderNumber;
/**
* @var DateTime
* @Groups({"order-export"})
*/
private $orderDate;
/**
* @var string
* @Groups({"order-export"})
*/
private $customerFullName;
/**
* @var string
* @Groups({"order-export"})
*/
private $customerEmail;
/**
* @var string
* @Groups({"order-export"})
<?php
namespace AppDTO;
use SymfonyComponentSerializerAnnotationGroups;
final class OrderExport
{
/**
* @var string
* @Groups({"order-export"})
*/
private $orderStatus;
/**
* @var string
* @Groups({"order-export"})
*/
private $orderNumber;
/**
* @var DateTime
* @Groups({"order-export"})
*/
private $orderDate;
/**
* @var string
* @Groups({"order-export"})
*/
private $customerFullName;
/**
* @var string
* @Groups({"order-export"})
*/
private $customerEmail;
/**
* @var string
* @Groups({"order-export"})
namespace AppDataTransformer;
use ApiPlatformCoreDataTransformerDataTransformerInterface;
…
class OrderExportDataTransformer implements DataTransformerInterface
{
/**
* @param Order $data
* @param string $to
* @param array $context
*
* @return object|void
*/
public function transform($data, string $to, array $context = [])
{
$export = new OrderExport();
$export->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;
}
}
Real-time applications
with API platform
• Redis + NodeJS + socket.io
• Pusher
• ReactPHP
• …
• but to be honest PHP is not build for realtime :)
Real-time applications
with API platform
• 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
resources:
AppEntityGreeting:
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));
}
Testing
• 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
Handy testing tools
• 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
PHP Matcher
Library that enables you to check your response against patterns.
PHP Matcher
Library that enables you to check your response against patterns.
Faker (/w Alice)
Library for generating random data
Postman tests
Newman + Postman
Postman tests + Newman
Newman + Postman
• 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
Api Platform (Symfony) is awesome!
Conclusion
Thank you!
Questions?
Antonio Perić-Mažar
t: @antonioperic
m: antonio@locastic.com

Contenu connexe

Tendances

Tendances (20)

IDENTITY ACCESS MANAGEMENT
IDENTITY ACCESS MANAGEMENTIDENTITY ACCESS MANAGEMENT
IDENTITY ACCESS MANAGEMENT
 
Cours design pattern m youssfi partie 7 facade bridge flyweight
Cours design pattern m youssfi partie 7 facade bridge flyweightCours design pattern m youssfi partie 7 facade bridge flyweight
Cours design pattern m youssfi partie 7 facade bridge flyweight
 
AEM hacker - approaching Adobe Experience Manager webapps in bug bounty programs
AEM hacker - approaching Adobe Experience Manager webapps in bug bounty programsAEM hacker - approaching Adobe Experience Manager webapps in bug bounty programs
AEM hacker - approaching Adobe Experience Manager webapps in bug bounty programs
 
Building Advanced XSS Vectors
Building Advanced XSS VectorsBuilding Advanced XSS Vectors
Building Advanced XSS Vectors
 
Solid principles
Solid principlesSolid principles
Solid principles
 
Postman Collection Format v2.0 (pre-draft)
Postman Collection Format v2.0 (pre-draft)Postman Collection Format v2.0 (pre-draft)
Postman Collection Format v2.0 (pre-draft)
 
Microsoft Defender for Endpoint Overview.pptx
Microsoft Defender for Endpoint Overview.pptxMicrosoft Defender for Endpoint Overview.pptx
Microsoft Defender for Endpoint Overview.pptx
 
[APIdays INTERFACE 2021] The Evolution of API Security for Client-side Applic...
[APIdays INTERFACE 2021] The Evolution of API Security for Client-side Applic...[APIdays INTERFACE 2021] The Evolution of API Security for Client-side Applic...
[APIdays INTERFACE 2021] The Evolution of API Security for Client-side Applic...
 
XSS Magic tricks
XSS Magic tricksXSS Magic tricks
XSS Magic tricks
 
Secure Credential Management with CredHub - DaShaun Carter & Sharath Sahadevan
Secure Credential Management with CredHub - DaShaun Carter & Sharath Sahadevan Secure Credential Management with CredHub - DaShaun Carter & Sharath Sahadevan
Secure Credential Management with CredHub - DaShaun Carter & Sharath Sahadevan
 
A Hacker's perspective on AEM applications security
A Hacker's perspective on AEM applications securityA Hacker's perspective on AEM applications security
A Hacker's perspective on AEM applications security
 
Gestion comptes bancaires Spring boot
Gestion comptes bancaires Spring bootGestion comptes bancaires Spring boot
Gestion comptes bancaires Spring boot
 
Cours design pattern m youssfi partie 3 decorateur
Cours design pattern m youssfi partie 3 decorateurCours design pattern m youssfi partie 3 decorateur
Cours design pattern m youssfi partie 3 decorateur
 
FIWARE Wednesday Webinars - How to Design DataModels
FIWARE Wednesday Webinars - How to Design DataModelsFIWARE Wednesday Webinars - How to Design DataModels
FIWARE Wednesday Webinars - How to Design DataModels
 
Hunting for security bugs in AEM webapps
Hunting for security bugs in AEM webappsHunting for security bugs in AEM webapps
Hunting for security bugs in AEM webapps
 
P3 listes et elements graphiques avancés
P3 listes et elements graphiques avancésP3 listes et elements graphiques avancés
P3 listes et elements graphiques avancés
 
Securing AEM webapps by hacking them
Securing AEM webapps by hacking themSecuring AEM webapps by hacking them
Securing AEM webapps by hacking them
 
P1 introduction à android
P1 introduction à androidP1 introduction à android
P1 introduction à android
 
Cross-Platform App Development with Flutter, Xamarin, React Native
Cross-Platform App Development with Flutter, Xamarin, React NativeCross-Platform App Development with Flutter, Xamarin, React Native
Cross-Platform App Development with Flutter, Xamarin, React Native
 
Hexagonal architecture with Spring Boot
Hexagonal architecture with Spring BootHexagonal architecture with Spring Boot
Hexagonal architecture with Spring Boot
 

Similaire à Using API Platform to build ticketing system #symfonycon

Building TweetEngine
Building TweetEngineBuilding TweetEngine
Building TweetEngine
ikailan
 
automation framework
automation frameworkautomation framework
automation framework
ANSHU GOYAL
 

Similaire à Using API Platform to build ticketing system #symfonycon (20)

Using API platform to build ticketing system (translations, time zones, ...) ...
Using API platform to build ticketing system (translations, time zones, ...) ...Using API platform to build ticketing system (translations, time zones, ...) ...
Using API platform to build ticketing system (translations, time zones, ...) ...
 
Don't worry be API with Slim framework and Joomla
Don't worry be API with Slim framework and JoomlaDon't worry be API with Slim framework and Joomla
Don't worry be API with Slim framework and Joomla
 
Building TweetEngine
Building TweetEngineBuilding TweetEngine
Building TweetEngine
 
Hexagonal architecture
Hexagonal architectureHexagonal architecture
Hexagonal architecture
 
API Workshop: Deep dive into REST APIs
API Workshop: Deep dive into REST APIsAPI Workshop: Deep dive into REST APIs
API Workshop: Deep dive into REST APIs
 
Introduction to Google App Engine
Introduction to Google App EngineIntroduction to Google App Engine
Introduction to Google App Engine
 
automation framework
automation frameworkautomation framework
automation framework
 
Checkmarx meetup API Security - API Security in depth - Inon Shkedy
Checkmarx meetup API Security - API Security in depth - Inon ShkedyCheckmarx meetup API Security - API Security in depth - Inon Shkedy
Checkmarx meetup API Security - API Security in depth - Inon Shkedy
 
Benefits of the CodeIgniter Framework
Benefits of the CodeIgniter FrameworkBenefits of the CodeIgniter Framework
Benefits of the CodeIgniter Framework
 
Swift meetup22june2015
Swift meetup22june2015Swift meetup22june2015
Swift meetup22june2015
 
Dependency Injection, Zend Framework and Symfony Container
Dependency Injection, Zend Framework and Symfony ContainerDependency Injection, Zend Framework and Symfony Container
Dependency Injection, Zend Framework and Symfony Container
 
Some tips to improve developer experience with Symfony
Some tips to improve developer experience with SymfonySome tips to improve developer experience with Symfony
Some tips to improve developer experience with Symfony
 
Google App Engine Overview - BarCamp Phnom Penh 2011
Google App Engine Overview - BarCamp Phnom Penh 2011Google App Engine Overview - BarCamp Phnom Penh 2011
Google App Engine Overview - BarCamp Phnom Penh 2011
 
flask.pptx
flask.pptxflask.pptx
flask.pptx
 
How React Native, Appium and me made each other shine @ContinuousDeliveryAmst...
How React Native, Appium and me made each other shine @ContinuousDeliveryAmst...How React Native, Appium and me made each other shine @ContinuousDeliveryAmst...
How React Native, Appium and me made each other shine @ContinuousDeliveryAmst...
 
How to build Simple yet powerful API.pptx
How to build Simple yet powerful API.pptxHow to build Simple yet powerful API.pptx
How to build Simple yet powerful API.pptx
 
SRV421 Deep Dive with AWS Mobile Services
SRV421 Deep Dive with AWS Mobile ServicesSRV421 Deep Dive with AWS Mobile Services
SRV421 Deep Dive with AWS Mobile Services
 
Developer Tutorial: WebAuthn for Web & FIDO2 for Android
Developer Tutorial: WebAuthn for Web & FIDO2 for AndroidDeveloper Tutorial: WebAuthn for Web & FIDO2 for Android
Developer Tutorial: WebAuthn for Web & FIDO2 for Android
 
Developing Chatbots with Google DialogFlow
Developing Chatbots with Google DialogFlowDeveloping Chatbots with Google DialogFlow
Developing Chatbots with Google DialogFlow
 
Codeigniter
CodeigniterCodeigniter
Codeigniter
 

Plus de Antonio Peric-Mazar

Plus de Antonio Peric-Mazar (20)

You call yourself a Senior Developer?
You call yourself a Senior Developer?You call yourself a Senior Developer?
You call yourself a Senior Developer?
 
Are you failing at being agile? #digitallabin
Are you failing at being agile? #digitallabinAre you failing at being agile? #digitallabin
Are you failing at being agile? #digitallabin
 
Symfony 4: A new way to develop applications #ipc19
Symfony 4: A new way to develop applications #ipc19Symfony 4: A new way to develop applications #ipc19
Symfony 4: A new way to develop applications #ipc19
 
A year with progressive web apps! #webinale
A year with progressive web apps! #webinaleA year with progressive web apps! #webinale
A year with progressive web apps! #webinale
 
The UI is the THE application #dpc19
The UI is the THE application #dpc19The UI is the THE application #dpc19
The UI is the THE application #dpc19
 
Symfony 4: A new way to develop applications #phpsrb
 Symfony 4: A new way to develop applications #phpsrb Symfony 4: A new way to develop applications #phpsrb
Symfony 4: A new way to develop applications #phpsrb
 
REST easy with API Platform
REST easy with API PlatformREST easy with API Platform
REST easy with API Platform
 
A year with progressive web apps! #DevConMU
A year with progressive web apps! #DevConMUA year with progressive web apps! #DevConMU
A year with progressive web apps! #DevConMU
 
Service workers are your best friends
Service workers are your best friendsService workers are your best friends
Service workers are your best friends
 
Progressive Web Apps are here!
Progressive Web Apps are here!Progressive Web Apps are here!
Progressive Web Apps are here!
 
Building APIs in an easy way using API Platform
Building APIs in an easy way using API PlatformBuilding APIs in an easy way using API Platform
Building APIs in an easy way using API Platform
 
Symfony4 - A new way of developing web applications
Symfony4 - A new way of developing web applicationsSymfony4 - A new way of developing web applications
Symfony4 - A new way of developing web applications
 
Build your business on top of Open Source
Build your business on top of Open SourceBuild your business on top of Open Source
Build your business on top of Open Source
 
Building APIs in an easy way using API Platform
Building APIs in an easy way using API PlatformBuilding APIs in an easy way using API Platform
Building APIs in an easy way using API Platform
 
Lessons learned while developing with Sylius
Lessons learned while developing with SyliusLessons learned while developing with Sylius
Lessons learned while developing with Sylius
 
Drupal8 for Symfony developers - Dutch PHP
Drupal8 for Symfony developers - Dutch PHPDrupal8 for Symfony developers - Dutch PHP
Drupal8 for Symfony developers - Dutch PHP
 
Drupal8 for Symfony Developers (PHP Day Verona 2017)
Drupal8 for Symfony Developers (PHP Day Verona 2017)Drupal8 for Symfony Developers (PHP Day Verona 2017)
Drupal8 for Symfony Developers (PHP Day Verona 2017)
 
Drupal8 for Symfony Developers
Drupal8 for Symfony DevelopersDrupal8 for Symfony Developers
Drupal8 for Symfony Developers
 
Maintainable + Extensible = Clean ... yes, Code!
Maintainable + Extensible = Clean ... yes, Code! Maintainable + Extensible = Clean ... yes, Code!
Maintainable + Extensible = Clean ... yes, Code!
 
A recipe for effective leadership
A recipe for effective leadershipA recipe for effective leadership
A recipe for effective leadership
 

Dernier

Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
Medical / Health Care (+971588192166) Mifepristone and Misoprostol tablets 200mg
 
Love witchcraft +27768521739 Binding love spell in Sandy Springs, GA |psychic...
Love witchcraft +27768521739 Binding love spell in Sandy Springs, GA |psychic...Love witchcraft +27768521739 Binding love spell in Sandy Springs, GA |psychic...
Love witchcraft +27768521739 Binding love spell in Sandy Springs, GA |psychic...
chiefasafspells
 
Large-scale Logging Made Easy: Meetup at Deutsche Bank 2024
Large-scale Logging Made Easy: Meetup at Deutsche Bank 2024Large-scale Logging Made Easy: Meetup at Deutsche Bank 2024
Large-scale Logging Made Easy: Meetup at Deutsche Bank 2024
VictoriaMetrics
 
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
masabamasaba
 
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
masabamasaba
 

Dernier (20)

%in Stilfontein+277-882-255-28 abortion pills for sale in Stilfontein
%in Stilfontein+277-882-255-28 abortion pills for sale in Stilfontein%in Stilfontein+277-882-255-28 abortion pills for sale in Stilfontein
%in Stilfontein+277-882-255-28 abortion pills for sale in Stilfontein
 
WSO2CON 2024 - Navigating API Complexity: REST, GraphQL, gRPC, Websocket, Web...
WSO2CON 2024 - Navigating API Complexity: REST, GraphQL, gRPC, Websocket, Web...WSO2CON 2024 - Navigating API Complexity: REST, GraphQL, gRPC, Websocket, Web...
WSO2CON 2024 - Navigating API Complexity: REST, GraphQL, gRPC, Websocket, Web...
 
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
 
What Goes Wrong with Language Definitions and How to Improve the Situation
What Goes Wrong with Language Definitions and How to Improve the SituationWhat Goes Wrong with Language Definitions and How to Improve the Situation
What Goes Wrong with Language Definitions and How to Improve the Situation
 
WSO2CON 2024 - Does Open Source Still Matter?
WSO2CON 2024 - Does Open Source Still Matter?WSO2CON 2024 - Does Open Source Still Matter?
WSO2CON 2024 - Does Open Source Still Matter?
 
WSO2CON 2024 - Building the API First Enterprise – Running an API Program, fr...
WSO2CON 2024 - Building the API First Enterprise – Running an API Program, fr...WSO2CON 2024 - Building the API First Enterprise – Running an API Program, fr...
WSO2CON 2024 - Building the API First Enterprise – Running an API Program, fr...
 
VTU technical seminar 8Th Sem on Scikit-learn
VTU technical seminar 8Th Sem on Scikit-learnVTU technical seminar 8Th Sem on Scikit-learn
VTU technical seminar 8Th Sem on Scikit-learn
 
Love witchcraft +27768521739 Binding love spell in Sandy Springs, GA |psychic...
Love witchcraft +27768521739 Binding love spell in Sandy Springs, GA |psychic...Love witchcraft +27768521739 Binding love spell in Sandy Springs, GA |psychic...
Love witchcraft +27768521739 Binding love spell in Sandy Springs, GA |psychic...
 
Architecture decision records - How not to get lost in the past
Architecture decision records - How not to get lost in the pastArchitecture decision records - How not to get lost in the past
Architecture decision records - How not to get lost in the past
 
WSO2CON2024 - It's time to go Platformless
WSO2CON2024 - It's time to go PlatformlessWSO2CON2024 - It's time to go Platformless
WSO2CON2024 - It's time to go Platformless
 
Large-scale Logging Made Easy: Meetup at Deutsche Bank 2024
Large-scale Logging Made Easy: Meetup at Deutsche Bank 2024Large-scale Logging Made Easy: Meetup at Deutsche Bank 2024
Large-scale Logging Made Easy: Meetup at Deutsche Bank 2024
 
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
 
%in Midrand+277-882-255-28 abortion pills for sale in midrand
%in Midrand+277-882-255-28 abortion pills for sale in midrand%in Midrand+277-882-255-28 abortion pills for sale in midrand
%in Midrand+277-882-255-28 abortion pills for sale in midrand
 
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
 
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
 
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
 
%in Bahrain+277-882-255-28 abortion pills for sale in Bahrain
%in Bahrain+277-882-255-28 abortion pills for sale in Bahrain%in Bahrain+277-882-255-28 abortion pills for sale in Bahrain
%in Bahrain+277-882-255-28 abortion pills for sale in Bahrain
 
tonesoftg
tonesoftgtonesoftg
tonesoftg
 
%in Rustenburg+277-882-255-28 abortion pills for sale in Rustenburg
%in Rustenburg+277-882-255-28 abortion pills for sale in Rustenburg%in Rustenburg+277-882-255-28 abortion pills for sale in Rustenburg
%in Rustenburg+277-882-255-28 abortion pills for sale in Rustenburg
 
Devoxx UK 2024 - Going serverless with Quarkus, GraalVM native images and AWS...
Devoxx UK 2024 - Going serverless with Quarkus, GraalVM native images and AWS...Devoxx UK 2024 - Going serverless with Quarkus, GraalVM native images and AWS...
Devoxx UK 2024 - Going serverless with Quarkus, GraalVM native images and AWS...
 

Using API Platform to build ticketing system #symfonycon

  • 1. Using API platform to build ticketing system Antonio Perić-Mažar, Locastic 22.11.2019. - #SymfonyCon, Amsterdam
  • 2. Antonio Perić-Mažar CEO @ Locastic Co-founder @ Litto Co-founder @ Tinel Meetup t: @antonioperic m: antonio@locastic.com
  • 3. Locastic Helping clients create web and mobile apps since 2011 • UX/UI • Mobile apps • Web apps • Training & Consulting www.locastic.com @locastic
  • 4. • 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
  • 5. • Social network • chat based • matching similar to Tinder :) • few CRM/ERP applications Context & API Platform Experience
  • 6. What is API platform ?
  • 7. –Fabien Potencier (creator of Symfony), SymfonyCon 2017 “API Platform is the most advanced API platform, in any framework or language.”
  • 8. • 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
  • 9. • 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:
  • 10. • 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:
  • 11. • invalidation-based HTTP caching • and basically everything needed to build modern APIs. API Platform built-in features:
  • 13. Create Entity Step One <?php // src/Entity/Greeting.php namespace AppEntity; class Greeting { private $id; public $name = ''; public function getId(): int { return $this->id; } }
  • 14. Create Mapping Step Two # config/doctrine/Greeting.orm.yml AppEntityGreeting: type: entity table: greeting id: id: type: integer generator: { strategy: AUTO } fields: name: type: string length: 100
  • 16. Expose Resource Step Four # config/api_platform/resources.yaml resources: AppEntityGreeting: ~
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 23.
  • 26. • 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
  • 27. // src/Entity/User.php namespace AppEntity; use SymfonyComponentSecurityCoreUserUserInterface; use SymfonyComponentSerializerAnnotationGroups; 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 … }
  • 28. // src/Entity/User.php namespace AppEntity; use SymfonyComponentSecurityCoreUserUserInterface; use SymfonyComponentSerializerAnnotationGroups; 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 AppEntityUser: type: entity table: users repositoryClass: AppRepositoryUserRepository id: id: type: integer generator: { strategy: AUTO } fields: email: type: string length: 255 password: type: string length: 255 roles: type: array
  • 29. // src/Entity/User.php namespace AppEntity; use SymfonyComponentSecurityCoreUserUserInterface; use SymfonyComponentSerializerAnnotationGroups; 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 AppEntityUser: type: entity table: users repositoryClass: AppRepositoryUserRepository 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: AppEntityUser: attributes: normalization_context: groups: ['user-read'] denormalization_context: groups: ['user-write']
  • 30. use ApiPlatformCoreDataPersisterContextAwareDataPersisterInterface; use AppEntityUser; use DoctrineORMEntityManagerInterface; use SymfonyComponentSecurityCoreEncoderUserPasswordEncoderInterface; 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(); }
  • 31. • 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)
  • 32.
  • 33.
  • 34. • 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
  • 35.
  • 36. User security checker Security <?php namespace AppSecurity; use AppExceptionAccountDeletedException; use AppSecurityUser as AppUser; use SymfonyComponentSecurityCoreExceptionAccountExpiredException; use SymfonyComponentSecurityCoreExceptionCustomUserMessageAuthenticat use SymfonyComponentSecurityCoreUserUserCheckerInterface; use SymfonyComponentSecurityCoreUserUserInterface; class UserChecker implements UserCheckerInterface { public function checkPreAuth(UserInterface $user) { if (!$user instanceof AppUser) { return; } // user is deleted, show a generic Account Not Found message. if ($user->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('...'); } } }
  • 37. User security checker Security # config/packages/security.yaml # ... security: firewalls: main: pattern: ^/ user_checker: AppSecurityUserChecker # ...
  • 38. Resource and operation level Security # api/config/api_platform/resources.yaml AppEntityBook: 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'
  • 39. Resource and operation level using Voters Security # api/config/api_platform/resources.yaml AppEntityBook: itemOperations: get: security_: 'is_granted('READ', object)' put: security_: 'is_granted('UPDATE', object)'
  • 40. • 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
  • 41. JWT tip A database-less user provider # config/packages/security.yaml security: providers: jwt: lexik_jwt: ~ security: firewalls: api: provider: jwt guard: # ...
  • 43. • 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
  • 44. <?php use LocasticApiPlatformTranslationBundleModelAbstractTranslatable; class Post extends AbstractTranslatable { // ... protected function createTranslation() { return new PostTranslation(); } }
  • 45. <?php use LocasticApiPlatformTranslationBundleModelAbstractTranslatable; class Post extends AbstractTranslatable { // ... protected function createTranslation() { return new PostTranslation(); } } use LocasticApiPlatformTranslationBundleModelAbstractTranslatable; class Post extends AbstractTranslatable { // ... /** * @Groups({"post_write", "translations"}) */ protected $translations; }
  • 46. <?php use LocasticApiPlatformTranslationBundleModelAbstractTranslatable; class Post extends AbstractTranslatable { // ... protected function createTranslation() { return new PostTranslation(); } } use LocasticApiPlatformTranslationBundleModelAbstractTranslatable; class Post extends AbstractTranslatable { // ... /** * @Groups({"post_write", "translations"}) */ protected $translations; } <?php use LocasticApiPlatformTranslationBundleModelAbstractTranslatable; use SymfonyComponentSerializerAnnotationGroups; class Post extends AbstractTranslatable { // ... /** * @var string * * @Groups({"post_read"}) */ private $title; public function setTitle($title) { $this->getTranslation()->setTitle($title); return $this; } public function getTitle() { return $this->getTranslation()->getTitle(); } }
  • 47. <?php use SymfonyComponentSerializerAnnotationGroups; use LocasticApiTranslationBundleModelAbstractTranslation; class PostTranslation extends AbstractTranslation { // ... /** * @var string * * @Groups({"post_read", "post_write", "translations"}) */ private $title; /** * @var string * @Groups({"post_write", "translations"}) */ protected $locale; public function setTitle($title) { $this->title = $title; return $this; } public function getTitle() { return $this->title; } }
  • 48. <?php use SymfonyComponentSerializerAnnotationGroups; use LocasticApiTranslationBundleModelAbstractTranslation; class PostTranslation extends AbstractTranslation { // ... /** * @var string * * @Groups({"post_read", "post_write", "translations"}) */ private $title; /** * @var string * @Groups({"post_write", "translations"}) */ protected $locale; public function setTitle($title) { $this->title = $title; return $this; } public function getTitle() { return $this->title; } } AppBundleEntityPost: 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']
  • 50. 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!" }
  • 51. 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" } } }
  • 52. • https://github.com/lexik/LexikTranslationBundle • or you can write your own: • https://locastic.com/blog/symfony-translation-process- automation/ Static translation
  • 53. Manipulating context and avoding to have / api/admin
  • 54. Manipulating the Context Context namespace AppEntity; use ApiPlatformCoreAnnotationApiResource; use SymfonyComponentSerializerAnnotationGroups; /** * @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; // ... }
  • 55. Manipulating the Context Context # api/config/services.yaml services: # ... 'AppSerializerBookContextBuilder': decorates: 'api_platform.serializer.context_builder' arguments: [ '@AppSerializerBookContextBuilder.inner' ] autoconfigure: false
  • 56. // api/src/Serializer/BookContextBuilder.php namespace AppSerializer; use ApiPlatformCoreSerializerSerializerContextBuilderInterface; use SymfonyComponentHttpFoundationRequest; use SymfonyComponentSecurityCoreAuthorizationAuthorizationCheckerInterface; use AppEntityBook; 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; } }
  • 58. • 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
  • 59.
  • 60. • 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
  • 61. CQRS Symfony Messenger & API Platform AppEntityPasswordResetRequest: collectionOperations: post: status: 202 itemOperations: [] attributes: messenger: true output: false
  • 62. CQRS Symfony Messenger & API Platform <?php namespace AppHandler; use AppEntityPasswordResetRequest; use SymfonyComponentMessengerHandlerMessageHandlerInterfac final class PasswordResetRequestHandler implements MessageHand { public function __invoke(PasswordResetRequest $forgotPassw { // do some heavy things } } <?php namespace AppEntity; final class PasswordResetRequest { public $email; }
  • 63. <?php namespace AppDataPersister; use ApiPlatformCoreDataPersisterContextAwareDataPersisterInterface; use AppEntityImageMedia; use DoctrineORMEntityManagerInterface; use SymfonyComponentMessengerMessageBusInterface; class ImageMediaDataPersister implements ContextAwareDataPersisterInterface { private $entityManager; private $messageBus; public function __construct(EntityManagerInterface $entityManager, MessageBusInterface $messageBus) { $this->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())); } }
  • 64. namespace AppEventSubscriber; use ApiPlatformCoreEventListenerEventPriorities; use AppEntityBook; use SymfonyComponentEventDispatcherEventSubscriberInterface; use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpKernelEventViewEvent; use SymfonyComponentHttpKernelKernelEvents; use SymfonyComponentMessengerMessageBusInterface; 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())); } }
  • 65.
  • 66.
  • 67. • 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
  • 68. { "body":"{"key":"TESTING","variables":{"UserName":"some test username ", "Greetings":"Some test greeting 4faf188d396e99e023a3f77c04a9c06d","Email": "antonio@locastic.com"},"to":"antonio@locastic.com"}", "properties":[ ], "headers":{ "type":"AppMessageCommunicationMessage", "X-Message-Stamp-SymfonyComponentMessengerStamp BusNameStamp":"[{"busName":"command_bus"}]", "X-Message-Stamp-SymfonyComponentMessengerStamp SentStamp":"[{"senderClass":"EnqueueMessengerAdapterQueueInteropTransport", "senderAlias":"communication_sync_amqp"}]", "Content-Type":"application/json" } }
  • 69. { "body":"{"key":"TESTING","variables":{"UserName":"some test username ", "Greetings":"Some test greeting 4faf188d396e99e023a3f77c04a9c06d","Email": "antonio@locastic.com"},"to":"antonio@locastic.com"}", "properties":[ ], "headers":{ "type":"AppMessageCommunicationMessage", "X-Message-Stamp-SymfonyComponentMessengerStamp BusNameStamp":"[{"busName":"command_bus"}]", "X-Message-Stamp-SymfonyComponentMessengerStamp SentStamp":"[{"senderClass":"EnqueueMessengerAdapterQueueInteropTransport", "senderAlias":"communication_sync_amqp"}]", "Content-Type":"application/json" } }
  • 70. { "body":"{"key":"TESTING","variables":{"UserName":"some test username ", "Greetings":"Some test greeting 4faf188d396e99e023a3f77c04a9c06d","Email": "antonio@locastic.com"},"to":"antonio@locastic.com"}", "properties":[ ], "headers":{ "type":"AppMessageCommunicationMessage", "X-Message-Stamp-SymfonyComponentMessengerStamp BusNameStamp":"[{"busName":"command_bus"}]", "X-Message-Stamp-SymfonyComponentMessengerStamp SentStamp":"[{"senderClass":"EnqueueMessengerAdapterQueueInteropTransport", "senderAlias":"communication_sync_amqp"}]", "Content-Type":"application/json" } } String contains escaped JSON
  • 71. { "key": "TESTING", "variables": { "username": "some test userName", "Grettings": "Some test greeting 4faf188d396e99e023a3f77c04a9c06d", "Email:" "antonio@locastic.com" } }
  • 72. namespace AppMessenger; … 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; } … }
  • 73. namespace AppMessenger; … 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; }
  • 74. namespace AppMessenger; … 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: AppMessengerExternalJsonMessageSerializer 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: ~ ...
  • 75. • 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:
  • 77. • 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
  • 79. • 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
  • 80. resources: AppEntityOrder: collectionOperations: get: ~ exports: method: POST path: '/orders/export' formats: csv: ['text/csv'] pagination_enabled: false output: "OrderExport::class" normalization_context: groups: ['order-export']
  • 81. <?php namespace AppDTO; use SymfonyComponentSerializerAnnotationGroups; final class OrderExport { /** * @var string * @Groups({"order-export"}) */ private $orderStatus; /** * @var string * @Groups({"order-export"}) */ private $orderNumber; /** * @var DateTime * @Groups({"order-export"}) */ private $orderDate; /** * @var string * @Groups({"order-export"}) */ private $customerFullName; /** * @var string * @Groups({"order-export"}) */ private $customerEmail; /** * @var string * @Groups({"order-export"})
  • 82. <?php namespace AppDTO; use SymfonyComponentSerializerAnnotationGroups; final class OrderExport { /** * @var string * @Groups({"order-export"}) */ private $orderStatus; /** * @var string * @Groups({"order-export"}) */ private $orderNumber; /** * @var DateTime * @Groups({"order-export"}) */ private $orderDate; /** * @var string * @Groups({"order-export"}) */ private $customerFullName; /** * @var string * @Groups({"order-export"}) */ private $customerEmail; /** * @var string * @Groups({"order-export"}) namespace AppDataTransformer; use ApiPlatformCoreDataTransformerDataTransformerInterface; … class OrderExportDataTransformer implements DataTransformerInterface { /** * @param Order $data * @param string $to * @param array $context * * @return object|void */ public function transform($data, string $to, array $context = []) { $export = new OrderExport(); $export->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; } }
  • 84.
  • 85. • Redis + NodeJS + socket.io • Pusher • ReactPHP • … • but to be honest PHP is not build for realtime :) Real-time applications with API platform
  • 86.
  • 87. • 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
  • 88. resources: AppEntityGreeting: 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)); }
  • 90. • 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
  • 92. • 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
  • 93.
  • 94. PHP Matcher Library that enables you to check your response against patterns.
  • 95. PHP Matcher Library that enables you to check your response against patterns.
  • 96. Faker (/w Alice) Library for generating random data
  • 98. Postman tests + Newman Newman + Postman
  • 99. • 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
  • 100.
  • 101. Api Platform (Symfony) is awesome! Conclusion