Slide 1

Slide 1 text

Un service, kézako ? Romaric Drigon @ AFUP Day 2021 Lille, 28/05/2021

Slide 2

Slide 2 text

Romaric Drigon, @romaricdrigon Responsable des développements à l'ASIT

Slide 3

Slide 3 text

Définition

Slide 4

Slide 4 text

« C’est un truc, qui fait quelque chose, plutôt de complexe » — RD, 2021 (probablement un lundi à 8h...)

Slide 5

Slide 5 text

Un web service ? Source: Borja Sotomayor, 2004-2005

Slide 6

Slide 6 text

Une classe dans un container 1. Programmation Orientée-Object 2. Injection de dépendances (DI) : $translator = new Translator(); $m = new Mailer($translator); 3. Container aussi appelé Service container 4. Service ! => une classe PHP fabien.potencier.org/what-is-dependency-injection.html martinfowler.com/articles/injection.html

Slide 7

Slide 7 text

À quoi cela sert ? 1. À ne pas se répéter $message = <<getEmail(), 'Votre inscription', $message, [ 'From' => '[email protected]', 'Reply-To' => '[email protected]', 'Content-type' => 'text/plain; utf-8', ]); mail($user->getEmail(), 'Confirmation de commande', 'Merci pour votre commande..', [ 'From' => '[email protected]', 'Reply-To' => '[email protected]', 'Content-type' => 'text/plain; utf-8', ]); class Mailer { public function send(User $to, string $subject, string $message): bool { return mail($to->getEmail(), $subject, $message, [ 'From' => '[email protected]', 'Reply-To' => '[email protected]', 'Content-type' => 'text/plain; utf-8', ]); } } $mailer = new Mailer(); $mailer->send($user, 'Votre inscription', /*...*/); $mailer = new Mailer(); $mailer->send($user, 'Confirmation de commande', /*...*/);

Slide 8

Slide 8 text

2. À simplifier l'utilisation $boundary = uniqid('boundary'); $message = <<

Confirmation d'inscription

Merci pour votre...

--$boundary-- EOT; mail($user->getEmail(), "Confirmation d'inscription", $message, [ 'From' => '[email protected]', 'Reply-To' => '[email protected]', 'Content-type' => "Content-Type: " ."multipart/alternative;boundary=$boundary", 'MIME-Version' =>'1.0', ]); class Mailer { public function send(User $to, string $subject, string $txt, string $html): bool { $message = <<getEmail(), $subject, $message, [ 'From' => '[email protected]', 'Reply-To' => '[email protected]', 'Content-type' => "Content-Type: multipart/alternative;...", 'MIME-Version' =>'1.0', ]); } } $mailer = new Mailer(); $mailer->send($user, 'Votre inscription', /*...*/);

Slide 9

Slide 9 text

3. À systématiser des règles class Mailer { public function send(User $to, string $subject, string $html): bool { if (!$user-getEmail() || !filter_var($user-getEmail(), FILTER_VALIDATE_EMAIL)) { throw new \RuntimeException('Adresse e-mail invalide'); } $txt = strip_tags($html); $message = <<getEmail(), $subject, $message, [ 'From' => '[email protected]', 'Reply-To' => '[email protected]', 'Content-type' => "Content-Type: multipart/...", 'MIME-Version' =>'1.0', ]); } }

Slide 10

Slide 10 text

4. À faciliter les tests class InscriptionService { public function __construct(private Mailer $mailer) {} public function register(string $email, string $password): bool { $user = new User(); // ... return $this->mailer->send($user, 'Votre inscription', /*...*/); } } class InscriptionServiceTest extends TestCase { public function test_it_sends_confirmation_email() { $mockMailer = $this->createMock(Mailer::class); $mockMailer->method('register')->willReturn(1); $sut = new InscriptionService($mockMailer); $this->assertTrue($sut->register('[email protected]', 'pa$$w0rd')); } }

Slide 11

Slide 11 text

5. À être appelé par le framework use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\KernelEvents; use Symfony\Component\HttpKernel\Event\RequestEvent; class MyHttpEventSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return [ KernelEvents::REQUEST => 'onIncomingRequest', ]; } public function onIncomingRequest(RequestEvent $event) { // ... } }

Slide 12

Slide 12 text

« C’est un objet, qui fait une opération complexe » — On retient donc quelques critères : On est dans l'action, pas dans les données Orchestre souvent des interactions entre di f férents sous composants Rend leur usage (ensemble) plus facile

Slide 13

Slide 13 text

Ce qu'un service n'est pas

Slide 14

Slide 14 text

Pour un container de service, tout est service ! # github.com/symfony/demo, config/services.yaml services: App\: resource: '../src/' exclude: - '../src/DependencyInjection/' - '../src/Entity/' - '../src/Kernel.php' - '../src/Tests/' App\Controller\: resource: '../src/Controller/' tags: ['controller.service_arguments']

Slide 15

Slide 15 text

Quelques exceptions — les points d'entrée — les classes qui contiennent les données — Entity/ dans notre exemple précédent — entités et value objects — éventuellement des classes "statiques" — Facade dans Laravel — exposant que des fonctions, par ex. Helper ou Utils => Str::random() dans Illuminate...

Slide 16

Slide 16 text

Service ? On n'a qu'une seule instance, service singleton ? class Cart { public function add(Product $product) {} public function remove(Product $product) {} public function empty() {} public function getTotal(): int {} } Data object ? Un service retourne un data object namespace Symfony\Component\HttpFoundation; class RequestStack { private array $requests = []; public function getCurrentRequest(): ?Request { return end($this->requests) ?: null; } // ... }

Slide 17

Slide 17 text

Un (mauvais) exemple

Slide 18

Slide 18 text

class UserAccountManager { public function __construct(Validator $v, Mailer $m, Database $db, PasswordHasher $hasher) { /* ... */ } public function create(string $email, string $password): Account { /* ... */ } public function confirm(string $confirmationToken); public function close(Account $account) {} public function changePassword(Account $account, string $newPassword) {} public function find(int $id): ?Account {} public function findByEmail(string $email): ?Account {} }

Slide 19

Slide 19 text

class UserAccountManager { public function __construct(Validator $v, Mailer $m, Database $db, PasswordHasher $hasher) { /* ... */ } public function create(string $email, string $password): Account { /* ... */ } public function confirm(string $confirmationToken); public function close(Account $account) {} public function changePassword(Account $account, string $newPassword) {} public function find(int $id): ?Account {} public function findByEmail(string $email): ?Account {} }

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

Problèmes ! Le service fait trop de choses ! — difficilement lisible (1000+ lignes...) — difficile à utiliser (nommage...) — trop de dépendances — tout est lié — difficilement testable (mocks...) Antipa!ern : god object

Slide 22

Slide 22 text

Principe de responsabilité uniqueSRP « Une classe ne doit avoir qu'une seule responsabilité. » => « Une seule raison de changer » Une solution : « Diviser pour mieux régner », spécialisez SRP "Single responbility principle", Robert C. Martin, 2005

Slide 23

Slide 23 text

Manager : un terme trop générique class UserAccountManager { public function create(string $email, string $password): Account { /* ... */ } // ==> AccountFactory public function changePassword(Account $account, string $newPassword) {} // ==> PasswordChanger public function find(int $id): ?Account {} public function findByEmail(string $email): ?Account {} // ==> AccountRepository } Ne me parlez plus de manager, sur le site de l'AFSY

Slide 24

Slide 24 text

Manager : un terme trop générique class UserAccountManager { public function create(string $email, string $password): Account { /* ... */ } // ==> AccountFactory public function changePassword(Account $account, string $newPassword) {} // ==> PasswordChanger public function find(int $id): ?Account {} public function findByEmail(string $email): ?Account {} // ==> AccountRepository } Ne me parlez plus de manager, sur le site de l'AFSY

Slide 25

Slide 25 text

Manager : un terme trop générique class UserAccountManager { public function create(string $email, string $password): Account { /* ... */ } // ==> AccountFactory public function changePassword(Account $account, string $newPassword) {} // ==> PasswordChanger public function find(int $id): ?Account {} public function findByEmail(string $email): ?Account {} // ==> AccountRepository } Ne me parlez plus de manager, sur le site de l'AFSY

Slide 26

Slide 26 text

Spaghe!i ≯ ravioli ≯ lasagne class AccountFactory { public function create(string $email, string $password) {} } class NewAccountNotifier { public function notify(/* ... */) {} } class AccountConfirmedNotifier { public function notify(/* ... */) {} } class PasswordChanger { public function change() {} } class PasswordChangedNotifier { public function notify(/* ... */) {} } class Mailer { public function send() {} } class AccountRepository { public function find() {} public function findByEmail() {} } class AccountSaver { public function persist() {} } class Database { public function exec() {} }

Slide 27

Slide 27 text

Quelques astuces Utiliser les frontières "naturelles" : — MVC : séparation données/ rendu — vocabulaire du framework — technique/métier — ... Limiter le nombre de dépendances (3-4 max) Ne pas prévoir à l'avance de la généricité, si le besoin n'est pas là. blog.codinghorror.com/rule-of-three/

Slide 28

Slide 28 text

Ne pas dépendre du contexte class NewAccountNotifier { private Request $request; // Injecté par le framework public function send(Account $new, string $adminEmail): void { if ('fr' === $this->request->getLocale()) { $subject = "Un nouveau compte a été ouvert par $new->getEmail()"; } else { $subject = "A new account was opened by $new->getEmail()"; } mail($adminEmail, $subject, /*...*/); } } Dépendance de la requête HTTP => ☠ le jour du passage en asynchrome (contexte CLI)

Slide 29

Slide 29 text

Un cas spécifique : service métier

Slide 30

Slide 30 text

« A SERVICE is an operation offered as an interface that stands alone in the model, without encapsulating state »vFR — Eric Evans, Domain-Driven Design (p. 105) vFR « Un Service est une opération exposée par une interface, interface qui fait partie du Modèle, et qui ne contient pas d'état. »

Slide 31

Slide 31 text

Un exemple "classique" class TransferProcessor { public function transfer(Account $source, Account $target, int $amount) { if ($source->getBalance() <= $amount) { throw new \DomainException(); } if ($source->getCurrency() !== $target->getCurrency()) { // ... } // ... } }

Slide 32

Slide 32 text

Distinction service métier1 / service d'infrastructure Inscription à l'AFUP Day 2021 : - gérer le formulaire (lire la requête HTTP...) => infrastructure - valider les données (pas inscrit en double...) => métier - créer une inscription => métier - sauvegarder l'inscription en base de donnée => infrastructure - composer un email de confirmation, un reçu => métier ? 1 Domain service en VO

Slide 33

Slide 33 text

Pas d'état / pas de données Opinion : un service métier ne doit pas avoir d'état, de données propres. => les données sont les joyaux de notre application ! Rend le service plus simple à utiliser : $manager = new StatefulBankAccountManager(); $manager->setSource($source); $manager->setTarget($target); $manager->setSource($source); // erreur ? $manager->transfer(1000); $manager = new StatelessBankAccountManager(); $manager->transfer($source, $target, 1000);

Slide 34

Slide 34 text

Implémentation

Slide 35

Slide 35 text

Nommer ses services Un service doit donc représenter "une chose que l'on fait", une seule responsabilité. => le nommer selon l'acteur correspondant Ex. "Mailer", "PaymentProcessor", "IssueMaker"... Dans certains cas, les designs patterns peuvent nous aiguiller sur comment exprimer cela dans notre code.

Slide 36

Slide 36 text

Pour la création : Factory class UserFactory { public function create(string $email, string $password): User { if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !$password) { throw new \RuntimeException(); } $hashed = password_hash($password); $user = new User(); $user->setEmail($email); $user->setPassword($hashed); return $user; } }

Slide 37

Slide 37 text

Pour la création2 : Builder namespace Symfony\Component\Form; interface FormBuilderInterface { public function add($child, string $type = null, array $options = []): self; public function setData($data): self; // ... // Pour récupérer le résultat public function getForm(): Form; } // Depuis un controller $form = $this->createFormBuilder() ->add('name', TextType::class) ->add('image', ImageType::class) ->add('createdAt', DateType::class) ->setData($blog) ->getForm();

Slide 38

Slide 38 text

Pour le requêtage : Repository interface ArticleRepository { public function find(int $id): ?Article; public function findAll(): array; public function findOnlineByBlog(Blog $blog): array; // Dans la définition de base (P of EAA, Fowler), // le repo est aussi utilisé pour la suppression. public function remove(Article $article); }

Slide 39

Slide 39 text

Comment découper structurer organiser ses services

Slide 40

Slide 40 text

Décoration ⭐ interface MailerInterface { public function send($to, $subject, $message); } class BasicMailer implements MailerInterface { /* ... */ } class LoggableMailer implements MailerInterface { public function __construct(private MailerInterface $mailer, private LoggerInterface $logger) {} public function send($to, $subject, $message) { $this->logger->info("Envoi d'un e-mail !"); $this->mailer->send($to, $subject, $message); } }

Slide 41

Slide 41 text

Évènements ⭐⭐ class AccountController { public function register(EventDispatcher $dispatcher): Response { $dispatcher->dispatch(new NewAccountEvent($account)); // ... } } class NewAccountListener implements EventSubscriberInterface { public function __construct(private MailerInterface $mailer) {} public function onNewAccount(NewAccountEvent $e) { $this->mailer->send($e->getEmail(), 'Bienvenue', /*...*/); } public function getSubscribedEvents() {} // ... } ⚠ Garder les listeners/subscribers découpés des services (métier)

Slide 42

Slide 42 text

Workflow ⭐⭐⭐ Image issue de la documentation symfony/workflow

Slide 43

Slide 43 text

Bus de commande ⭐⭐⭐⭐ speakerdeck.com/romaricdrigon/eviter-un-controleur-de-1000-lignes-vite-un-bus-de-commande Implémentation avec tactician.thephpleague.com

Slide 44

Slide 44 text

Récapitulatif Un service : — une classe PHP — une seule responsabilité — séparer les services des données — séparer du contexte — séparer de l'infrastructure — si flou, ne pas trop diviser dès le début — les design patterns peuvent aider

Slide 45

Slide 45 text

Questions ? @romaricdrigon / romaricdrigon.github.io