Symfony 5

Symfony 5

364d59ac0b4b4e5eee8aeb27a127d176?s=128

Titouan Galopin

November 13, 2019
Tweet

Transcript

  1. Symfony 5 Titouan GALOPIN

  2. 2 Titouan Galopin Product Manager SymfonyInsight insight.symfony.com

  3. Agenda 1. Symfony 5 2. Messenger / Mercure 3. Mailer

    / Notifier 4. HttpClient 5. Autres nouveautés 3
  4. 4 1. Symfony 5

  5. 5

  6. 6 PHP 7.2+

  7. 7 Versionnement sémantique

  8. 8 Symfony 5 = 4.4 sans les dépréciations

  9. 9 Evolution, pas Révolution Symfony 5 est proche de Symfony

    4
  10. 10 Mais Symfony 5, c’est aussi de nouvelles fonctionnalités

  11. 11 La démarche : rendre Symfony plus simple, agréable et

    productif
  12. 12 Comment ? DX, DX, DX

  13. 13 Symfony 5 reprend les bases de Symfony 4 et

    y intègre de nouveaux composants
  14. 14 L’objectif ? Construire des abstractions haut-niveau au dessus de

    nos abstractions bas-niveau
  15. 15 2. Messenger Mercure

  16. 16 L’asynchrone, c’est génial

  17. 17 Mais en fait, pourquoi ?

  18. 18 Depuis l’invention de PHP, deux modèles s’opposent

  19. 19 Java, node, … Un process réutilisé Process Java, node,

    ... Requête 1 PHP Un process par reqûete Évolution du temps Requête 2 Requête 3 Requête 4 ... Process PHP Requête 1 Requête 2 Requête 3 Requête 4 ... Process PHP Process PHP Process PHP
  20. 20 Java, node, … Un process réutilisé PHP Un process

    par reqûete État partagé = Plus performant Sans état = Plus facile à maintenir et à développer
  21. 21 Java, node, … Un process réutilisé PHP Un process

    par reqûete État partagé = Plus performant Sans état = Plus facile à maintenir et à développer Avantage technique Avantage humain
  22. 22 Et si on faisait les deux ?

  23. 23 L’asynchrone n’est pas utile partout

  24. 24 Les consumers

  25. 25 PHP Un process par reqûete ... + un traitement

    en arrière-plan quand c’est utile Process PHP Requête 1 Requête 2 Requête 3 Process PHP Process PHP Process Consumer PHP
  26. 26 Envoyer des e-mails Générer des miniatures Synchroniser avec une

    API ...
  27. 27 Messenger = Une librairie pour créer et utiliser des

    consumers
  28. 28 composer require symfony/messenger

  29. 29 Process PHP Requête 1 Consumer PHP RabbitMQ, Redis, PDO,

    ... Consumer PHP Process PHP Requête 1 Process PHP Requête 1 ...
  30. 30 Process PHP Requête 1 Consumer PHP RabbitMQ, Redis, PDO,

    ... Consumer PHP Process PHP Requête 1 Process PHP Requête 1 ... Messenger utilise un intermédiaire de traitement des messages
  31. 31 Process PHP Requête 1 Consumer PHP RabbitMQ, Redis, PDO,

    ... Consumer PHP Process PHP Requête 1 Process PHP Requête 1 ... Messenger vous aide à créer des Messages depuis vos process HTTP
  32. 32 Process PHP Requête 1 Consumer PHP RabbitMQ, Redis, PDO,

    ... Consumer PHP Process PHP Requête 1 Process PHP Requête 1 ... Et à traiter ces Messages grâce à des Handlers utilisés par les Consumers
  33. 33 framework: messenger: failure_transport: failed transports: async: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' retry_strategy:

    max_retries: 3 delay: 1000 multiplier: 2 failed: 'doctrine://default?queue_name=failed' routing: 'Symfony\Component\Mailer\Messenger\SendEmailMessage': async 'App\Messenger\Mailchimp\MailchimpSynchronizeMessage': async
  34. 34 framework: messenger: failure_transport: failed transports: async: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' retry_strategy:

    max_retries: 3 delay: 1000 multiplier: 2 failed: 'doctrine://default?queue_name=failed' routing: 'Symfony\Component\Mailer\Messenger\SendEmailMessage': async 'App\Messenger\Mailchimp\MailchimpSynchronizeMessage': async Utiliser de l’asynchrone pour ces messages (Messenger peut aussi fonctionner en synchrone)
  35. 35 framework: messenger: failure_transport: failed transports: async: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' retry_strategy:

    max_retries: 3 delay: 1000 multiplier: 2 failed: 'doctrine://default?queue_name=failed' routing: 'Symfony\Component\Mailer\Messenger\SendEmailMessage': async 'App\Messenger\Mailchimp\MailchimpSynchronizeMessage': async Réessayer 3 fois (1 seconde, 2 secondes et 4 secondes plus tard)
  36. 36 framework: messenger: failure_transport: failed transports: async: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' retry_strategy:

    max_retries: 3 delay: 1000 multiplier: 2 failed: 'doctrine://default?queue_name=failed' routing: 'Symfony\Component\Mailer\Messenger\SendEmailMessage': async 'App\Messenger\Mailchimp\MailchimpSynchronizeMessage': async Si un message ne fonctionne finalement pas, le stocker dans une table
  37. 37 public function index(MessageBusInterface $bus) { // ... $this->bus->dispatch(new MailchimpAddEmailMessage($email));

    } Votre contrôleur N’importe quel objet créé par vous (et contenant donc toutes les informations que vous voulez)
  38. 38 use Symfony\Component\Messenger\Handler\MessageHandlerInterface; class MailchimpSynchronizeHandler implements MessageHandlerInterface { public function

    __invoke(MailchimpAddEmailMessage $message) { // ... } } Votre handler Le handler à utiliser est déterminé grâce au typehint du message Le handler est enregistré grâce à l’auto-configuration
  39. 39 php bin/console messenger:consume

  40. 40 Mais comment retourner un résultat au client ?

  41. 41 Mercure https://symfony.com/doc/current/mercure.html

  42. 42 Un binaire en Go Transmet les messages de votre

    consumer à vos clients
  43. 43 Process PHP Client Consumer PHP Envoi de l’e-mail RabbitMQ,

    Redis, PDO, ... Réponse simple Message d’e-mail Message d’e-mail Message Mercure Process Mercure Message Mercure Requête initiale Connexion W S à M ercure Message Mercure
  44. 44 4. HttpClient

  45. 45 Comment fait-on une requête HTTP en PHP ?

  46. 46 fopen

  47. 47 $url = 'https://symfony.com/versions.json'; $response = fopen($url, 'rb'); $versions =

    json_decode( stream_get_contents($response), true );
  48. 48 Méthode ? Headers ? SSL ?

  49. 49 Compliqué ...

  50. 50 ext-curl

  51. 51 $url = 'https://symfony.com/versions.json'; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url);

    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $versions = json_decode(curl_exec($ch), true); curl_close($ch);
  52. 52 Mieux ! Plus complet et fonctionne plutôt bien par

    défaut
  53. 53 Mais ... Complexe d’utilisation Difficile à mocker Requiert l’extension

  54. 54 ...

  55. 55 Les clients HTTP

  56. 56 Guzzle 3 Buzz Httplug Httpful PHP-HTTP PSR-18 Guzzle 5

    Guzzle 6
  57. 57 Qui a déjà développé une librairie réutilisable utilisant un

    client HTTP ?
  58. 58 3 packages virtuels php-http/client-implementation:^1.0 php-http/client-implementation:^2.0 psr/http-client-implementation

  59. 59 Incompatibles entre eux donc impossible de créer un client

    pour les 3
  60. 60 PSR18 $url = 'https://symfony.com/versions.json'; $request = $client->createRequest('GET', $url); $response

    = $client->sendRequest($request); $versions = json_decode( $response->getBody()->getContents(), true );
  61. 61 interface ClientInterface { /** * Sends a PSR-7 request

    and returns a PSR-7 response. * * @param RequestInterface $request * * @return ResponseInterface * * @throws \Psr\Http\Client\ClientExceptionInterface If an error happens * while processing the request. */ public function sendRequest(RequestInterface $request): ResponseInterface; }
  62. 62 Asynchrone ? HTTP 2/3 ? Push ? Streams ?

    Options et erreurs de transport ?
  63. 63 Non-défini dans les interfaces = Inutilisable

  64. 64 Peut-on améliorer la situation ?

  65. 65 composer require symfony/http-client

  66. 66 Fournit des décorateurs pour tous les packages virtuels

  67. 67 Symfony HttpClient = Un contrat = Une interface

  68. 68 interface HttpClientInterface { public const OPTIONS_DEFAULTS = [...]; public

    function request( string $method, string $url, array $options = [] ): ResponseInterface; public function stream( $responses, float $timeout = null ): ResponseStreamInterface; }
  69. 69 interface ResponseInterface { public function getStatusCode(): int; public function

    getHeaders($throw = true): array; public function getContent($throw = true): string; public function toArray($throw = true): array; public function cancel(): void; public function getInfo(): array; }
  70. 70 use Symfony\Component\HttpClient\HttpClient; $client = HttpClient::create(); $url = 'https://symfony.com/versions.json'; $response

    = $client->request('GET', $url); $versions = $response->toArray();
  71. 71 /** * @throws TransportExceptionInterface * When a network error

    occurs * * @throws RedirectionExceptionInterface * On a 3xx when $throw is true and the * "max_redirects" option has been reached * * @throws ClientExceptionInterface * On a 4xx when $throw is true * * @throws ServerExceptionInterface * On a 5xx when $throw is true */ Comportement en cas d’erreur du transport défini dans l’interface
  72. 72 Options du transport défini dans l’interface : créer des

    décorateurs réellement découplés est possible
  73. 73 Asynchrone

  74. 74 use Symfony\Component\HttpClient\HttpClient; $client = HttpClient::create(); $responses = []; for

    ($i = 1; $i <= 100; $i++) { $responses[] = $client->request('GET', 'https://example.com/?p='.$i); } foreach ($responses as $response) { // block until completion of $response // but monitor all other responses meanwhile echo $response->getContent(); } ResponseInterface lazy
  75. 75 Streamable

  76. 76 $url = 'http://.../ubuntu-18.04.1-desktop-amd64.iso'; $response = $client->request('GET', $url); // Responses

    are lazy! $h = fopen('./ubuntu.iso', 'wb'); foreach ($client->stream($response) as $chunk) { fwrite($h, $chunk->getContent()); }
  77. 77 HTTP 2 Push natif

  78. 78 4 décorateurs natifs • ScopingHttpClient Configuration d’options par défaut

    par domaine • MockHttpClient Client qui n’effectue pas les requêtes • CachingHttpClient Ajoute la gestion du cache HTTP • HttplugClient / Psr18Client Pour la compatibilité avec les packages virtuels
  79. 79 Dans Symfony Service http_client Autowiring Configuration YAML

  80. 80 class MailchimpClient { private $httpClient; public function __construct(HttpClientInterface $httpClient)

    { $this->httpClient = $httpClient; } // ... }
  81. 81 framework: http_client: max_host_connections: 4 default_options: # ... scoped_clients: github_client:

    base_uri: https://api.github.com headers: Authorization: token abc123 # creates the HttpClientInterface $githubClient autowiring alias # and the HttpClientInterface $scopingHttpClient one
  82. 82 3. Mailer Notifier

  83. 83 Symfony 5 : Fabriquer de nouvelles abstractions haut-niveau au

    dessus de celles bas-niveau
  84. 84 SwiftMailer => Symfony Mailer HttpClient Messenger Twig Dotenv

  85. 85 Grâce aux abstractions bas-niveau, on peut faire beaucoup plus

    de choses en moins de code
  86. 86 composer require symfony/mailer

  87. 87 class MailerController extends AbstractController { public function sendEmail(MailerInterface $mailer)

    { $email = (new Email()) ->from('hello@example.com') ->to('you@example.com') ->subject('Time for Symfony Mailer!') ->text('Sending emails is fun again!') ->html('<p>See Twig integration for better HTML integration!</p>'); $mailer->send($email); // ... } }
  88. 88 Dotenv Configurer le Mailer différemment selon l’environnement

  89. 89 // SMTP MAILER_DSN=smtp://user:pass@smtp.example.com

  90. 90 // SMTP MAILER_DSN=smtp://user:pass@smtp.example.com // Sendgrid // composer require symfony/sendgrid-mailer

    MAILER_DSN=smtp://$SENDGRID_KEY@sendgrid // Gmail // composer require symfony/google-mailer MAILER_DSN=smtp://$USERNAME:$PASSWORD@gmail // …
  91. 91 // SMTP MAILER_DSN=smtp://user:pass@smtp.example.com // Sendgrid // composer require symfony/sendgrid-mailer

    MAILER_DSN=smtp://$SENDGRID_KEY@sendgrid // Gmail // composer require symfony/google-mailer MAILER_DSN=smtp://$USERNAME:$PASSWORD@gmail // … // High Availability: si le premier a une erreur, utiliser le deuxième MAILER_DSN='api://id@postmark || smtp://key@sendgrid' // Load Balancing: répartir les mails sur chaque provider MAILER_DSN='api://id@postmark && smtp://key@sendgrid'
  92. 92 // SMTP MAILER_DSN=smtp://user:pass@smtp.example.com // Sendgrid // composer require symfony/sendgrid-mailer

    MAILER_DSN=smtp://$SENDGRID_KEY@sendgrid // Gmail // composer require symfony/google-mailer MAILER_DSN=smtp://$USERNAME:$PASSWORD@gmail // … // High Availability: si le premier a une erreur, utiliser le deuxième MAILER_DSN='api://id@postmark || smtp://key@sendgrid' // Load Balancing: répartir les mails sur chaque provider MAILER_DSN='api://id@postmark && smtp://key@sendgrid' HttpClient
  93. 93 Twig Des e-mails riches, responsives et compatibles avec tous

    les lecteurs
  94. 94 use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Mime\Address; $email = (new TemplatedEmail()) ->from('fabien@example.com')

    ->to(new Address('ryan@example.com')) ->subject('Thanks for signing up!') // path of the Twig template to render ->htmlTemplate('emails/signup.html.twig') // pass variables (name => value) to the template ->context([ 'expiration_date' => new \DateTime('+7 days'), 'username' => 'foo', ]) ;
  95. 95 Embarquer les images # config/packages/twig.yaml twig: paths: # point

    this wherever your images live '%kernel.project_dir%/assets/images': images {# '@images/' refers to the Twig namespace #} <img src="{{ email.image('@images/logo.png') }}" alt="Logo">
  96. 96 Inliner le CSS composer require twig/extra-bundle twig/cssinliner-extra # config/packages/twig.yaml

    twig: paths: '%kernel.project_dir%/assets/css: css {% apply inline_css(source('@css/email.css')) %} <h1>Welcome {{ username }}!</h1> {# ... #} {% endapply %}
  97. 97 Inky : responsive et cross-platform composer require twig/extra-bundle twig/inky-extra

    {% apply inky_to_html %} <container> <row class="header"> <columns> <spacer size="16"></spacer> <h1 class="text-center">Welcome {{ email.toName }}!</h1> </columns> {# ... #} </row> </container> {% endapply %}
  98. 98 Messenger Des e-mails envoyés en asynchrone

  99. 99 # config/packages/messenger.yaml framework: messenger: transports: async: "%env(MESSENGER_TRANSPORT_DSN)%" routing: 'Symfony\Component\Mailer\Messenger\SendEmailMessage':

    async
  100. 100 Les e-mails sont des messages ...

  101. 101 Les SMS sont des messages ...

  102. 102 Les notifications sont des messages ...

  103. 103 Symfony Notifier

  104. 104 Reprend les principes de Mailer et les adapte à

    d’autres méthodes d’envoi
  105. 105 class SmsController extends AbstractController { public function sendSms(TexterInterface $texter)

    { $sms = new SmsMessage('+33606060606', 'New subscription started!'); $texter->send($sms); // ... } }
  106. 106 class SmsController extends AbstractController { public function sendSms(TexterInterface $texter)

    { $sms = new SmsMessage('+33606060606', 'New subscription started!'); $texter->send($sms); // ... } } DSN : Twilio, nextmo, ...
  107. 107 class ChatController extends AbstractController { public function sendMessage(ChatterInterface $chatter)

    { $message = new ChatMessage('New subscription started!'); $chatter->send($message); // ... } }
  108. 108 class ChatController extends AbstractController { public function sendMessage(ChatterInterface $chatter)

    { $message = new ChatMessage('New subscription started!'); $chatter->send($message); // ... } } DSN : Slack, Telegram, ...
  109. 109 Une même infrastructure Abstraction haut-niveau (MailerInterface, TexterInterface, ChatterInterface, …)

    Votre code Messenger Twig Transport (Dotenv, HttpClient)
  110. 110 Une même infrastructure Abstraction haut-niveau (MailerInterface, TexterInterface, ChatterInterface, …)

    Votre code Messenger Twig Transport (Dotenv, HttpClient) Notifier
  111. 111 class NotificationController extends AbstractController { public function send(NotifierInterface $notifier)

    { $notification = new Notification( 'New subscription started!', ['sms', 'chat/slack', 'email'] ); $notifier->send( $notification, new Receiver('titouan@symfony.com') ); // ... } }
  112. 112 5. Autres nouveautés

  113. 113 Composant String

  114. 114 composer require symfony/string

  115. 115 use Symfony\Component\String\UnicodeString; $text = (new UnicodeString('This is a déjà-vu

    situation.')) ->trimEnd('.') ->replace('déjà-vu', 'jamais-vu') ->append('!'); // $text = 'This is a jamais-vu situation!' $content = new UnicodeString('नमस्ते दु नया'); if ($content->ignoreCase()->startsWith('नमस्ते')) { // ... }
  116. 116 use Symfony\Component\String\UnicodeString; $text = u('This is a déjà-vu situation.')

    ->trimEnd('.') ->replace('déjà-vu', 'jamais-vu') ->append('!'); // $text = 'This is a jamais-vu situation!' $content = u('नमस्ते दु नया'); if ($content->ignoreCase()->startsWith('नमस्ते')) { // ... }
  117. 117 Locale-aware slugger

  118. 118 class SlugController extends AbstractController { public function index(SluggerInterface $slugger)

    { $slug = $slugger->slug('Стойността трябва да бъде лъжа'); // ... } }
  119. 119 class SlugController extends AbstractController { public function index(SluggerInterface $slugger)

    { $slug = $slugger->slug('Стойността трябва да бъде лъжа'); // ... } } Transliteration Basé sur la locale de la requête
  120. 120 Security algorithm: auto

  121. 121 Avant # config/packages/security.yaml security: # ... encoders: App\Entity\User: algorithm:

    'bcrypt' Après # config/packages/security.yaml security: # ... encoders: App\Entity\User: algorithm: 'auto'
  122. 122 Et bien d’autres ! https://symfony.com/blog/category/living-on-the-edge

  123. insight.symfony.com // support@symfony.com Upgrade to Symfony 5 with confidence

  124. Merci ! 124 Des questions ? ▪ @titouangalopin sur Twitter

    ▪ titouan.galopin @symfony.com