Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Symfony 5 - AFUP Day Lille 2020

Symfony 5 - AFUP Day Lille 2020

Comme tous les deux ans, novembre 2019 a été l'occasion d'une nouvelle version majeure de Symfony. Cette version 5 est particulièrement intéressante de part la démarche qui l'anime : mieux résoudre les problèmes des développeurs et développeuses, en créant des librairies haut-niveau basées sur les fondations solides de Symfony 4. Je vous propose de découvrir ou redécouvrir ensemble les nouveautés apparues dans Symfony depuis la version 4.0 (Messenger, HttpClient, Mailer, Notifier, String, ...) et en quoi ces nouveautés changent la façon dont nous avons de concevoir le futur de Symfony.

364d59ac0b4b4e5eee8aeb27a127d176?s=128

Titouan Galopin

June 26, 2020
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 Asynchrone

  72. 72 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
  73. 73 Streamable

  74. 74 $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()); }
  75. 75 HTTP 2 Push natif

  76. 76 Dans Symfony Service http_client Autowiring Configuration YAML

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

    { $this->httpClient = $httpClient; } // ... }
  78. 78 3. Mailer Notifier

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

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

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

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

  83. 83 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); // ... } }
  84. 84 Dotenv Configurer le Mailer différemment selon l’environnement

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

  86. 86 // 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 // …
  87. 87 // 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'
  88. 88 // 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
  89. 89 Twig Des e-mails riches, responsives et compatibles avec tous

    les lecteurs
  90. 90 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', ]) ;
  91. 91 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">
  92. 92 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 %}
  93. 93 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 %}
  94. 94 Messenger Des e-mails envoyés en asynchrone

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

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

  97. 97 Les SMS sont des messages ...

  98. 98 Les notifications sont des messages ...

  99. 99 Symfony Notifier

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

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

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

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

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

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

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

    Votre code Messenger Twig Transport (Dotenv, HttpClient) Notifier
  107. 107 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') ); // ... } }
  108. 108 5. Autres nouveautés

  109. 109 Composant String

  110. 110 composer require symfony/string

  111. 111 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('नमस्ते')) { // ... }
  112. 112 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('नमस्ते')) { // ... }
  113. 113 Locale-aware slugger

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

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

    { $slug = $slugger->slug('Стойността трябва да бъде лъжа'); // ... } } Transliteration Basé sur la locale de la requête
  116. 116 Et bien d’autres ! https://symfony.com/blog/category/living-on-the-edge

  117. insight.symfony.com // support@symfony.com Jeudi 9 juillet Mettre en place des

    process qualité avec Symfony Partons à la découverte des process qualité de Symfony en tant que framework pour les appliquer à nos propres applications et ainsi faciliter leur maintenance. Rendez-vous sur @symfonyinsight (Twitter) pour vous inscrire !
  118. Merci ! 118 Des questions ? ▪ @titouangalopin sur Twitter

    ▪ titouan.galopin @symfony.com