Gestion des utilisateurs sans FosUserBundle 2ème partie

Ici, on attaque les choses sérieuses avec des events, envoi de mails, services, validateurs et surtout, découplage maximum et réutilisabilité du code.

L’inscription

Un utilisateur doit pouvoir s’inscrire. Pour ce faire, nous allons déjà commencer par créer notre entité Registration.php

Cette entité va nous servir pour le formulaire d’inscription. La fonction getUser trouvera son utilité un peu plus tard, lorsque le formulaire sera soumis.

Créer un validateur d’email et de username unique

Nous devons vérifier à l’enregistrement s’il n’existe pas en base un autre utilisateur ayant le même email ou le même username.

Cette vérification est possible dans le contrôleur, mais les validateurs de contrainte personnalisés sont faits pour ça.

Mettre à jour le fichier de services:

 

Le RegistrationType

 

Le RegisterController

Il faut à présent soumettre ce formulaire, enregistrer l’utilisateur, lancer l’event d’envoi du mail. Il n’est pas question de mettre tout cela dans le contrôleur. Car bien évidemment, ceci serait possible:

Vous voyez à présent que la fonction getUser() de Registration nous permet de créer un User et de les setter les valeurs passées en POST sans avoir à le faire dans le contrôleur.

Nous faisons faire énormément de choses à notre fonction:

  1. Traitement des infos de la requête
  2. Encodage du mot de passe
  3. Persistance et flush du User créé avec les valeurs de la request
  4. Envoi de l’email

Il va falloir éclater toutes ces responsabilités en plusieurs fonctions. En effet, que se passerait-il si nous voulions faire d’autres choses qu’envoyer un mail (par exemple loguer l’enregistrement). De même, et si nous voulions que ce soit autre chose que l’entité User qui soit sauvée? Il faudrait revenir sur le contrôleur, et ce n’est pas une solution viable, car il faut au maximum éviter de revenir sur du code déjà écrit et testé.

Occupons-nous d’abord de créer un manager de User (qui le créera, le persistera et flushera, encodera le mot de passe, etc.) ainsi que son interface.

 

Vous remarquerez que c’est UserInterface qui est passé en argument des fonctions createUser et persistAndFlushUser, et non User, ceci toujours dans le but de respecter la norme O de SOLID. Nous pouvons ici passer n’importe quelle instance d’objet d’une classe qui implémente UserInterface.

Ici, je passe déjà le dispatcher pour lancer l’événement de l’envoi d’e-mail (nous allons créer cet événement plus tard)

Du côté du services.xml

 

Le code de mon contrôleur RegisterController devient ceci:

Mais c’est loin d’être suffisant. Notre code est toujours fortement couplé avec une entité de UserInterface (et si je veux enregistrer autre chose qu’une instance de cette classe?)… et le contrôleur peut encore être allégé. Que cela ne tienne, nous allons créer un RegisterFormHandler qui implémentera une FormHandlerInterface.

 Le FormHandleInterface

Elle ne comporte qu’une seule méthode, handle, qui prend en paramètre FormInterface, une request et un tableau d’options qui peut être vide.

 Le RegisterFormHandler

Plutôt simple, celui-ci va implémenter FormHandlerInterface, traiter la requête et appeler une fonction du UserManager pour créer le user. Sa fonction handle nous renvoie un booléen pour nous dire si la requête était bien valide.

Mise à jour du services.xml

Mise à jour du RegisterController

Et là, notre registerAction s’allège encore :

Les events

Il nous reste une dernière chose à faire: créer un mail pour informer l’utilisateur que son compte a bien été créé et qu’il peut à présent se connecter sur la plateforme.

Inutile de vous dire que nous n’allons pas faire cela dans le contrôleur. C’est le userManager qui va se charger de dispatcher l’événement et nous allons dire à Symfony que lorsque cette événement est dispatché, il faut que tous ses listeners qui sont branchés dessus exécutent une action en particulier (c’est le principe du Design Pattern Observer). Pour cela, Symfony propose deux systèmes: les listeners et les subscribers. Ceci fera l’objet d’un autre article.

Créer un évent

Il s’agit simplement d’une classe concernant l’entité dont nous voulons récupérer des infos (le sujet, celui qui est observé par les listeners, en a besoin). Je vais simplement l’appeler UserDataEvent (mais vous pouvez lui donner le nom de l’événement, comme NewAccountUserEvent par exemple, mais je vais rester générique ici)

Etablir la liste des events de l’application

Ceci se faire dans un fichier et est, en ce qui me concerne, une bonne pratique pour connaître tous les events de votre application.

Dispatcher l’événement

La fonction createUser du UserManager peut à présent dispatcher l’événement:

Créer le listener d’envoi de mail

Il va prendre en argument switft_mailer, le templating de twig, un template et une adresse mail:

Vous pourriez vous demander comment la méthode onNewAccountCreated peut récupérer les informations de l’event? C’est simplement lors du dispatch: nous avons passé un second argument qui était ce fameux UserDataEvent.

Création du mail

Nous définissons ici deux blocks qui sont renseignés dans la fonction onNewAccountCreated:

 Enregistrement du listener au niveau du fichier services.xml

A présent que nous dispatchons l’événement à la création d’un utilisateur et que nous avons créé l’événement en question, il est temps de raccorder l’écouteur à celui qui l’informe par le fichier des services:

Ici, nous indiquons à Symfony  que nous avons un listener (avec le tag  kernel.event_listener), qui se déclenche à l’événement app.new_account_created, et que nous voulons exécuter la méthode onNewAccountCreated lorsqu’il est dispatché.

Ici, mail_from est un paramètre que j’ai rajouté dans parameters.yml.

Le template

 

Le code github se trouve ici: https://github.com/jpsymfony/authentication-demo

Rédigé par

5 comments

  1. Bonjour et merci pour ces articles de qualité !

    Je suis encore en phase d’apprentissage avec le framework Symfony (mais je pense qu’avec des outils aussi poussé on ne sort jamais de cette dite phase :P).

    Je ne comprends pas bien le concept du tout déléguer en Handler pour la partie Registration. Je connais bien les bénéfices du « diviser pour mieux régner » en POO, mais là j’avoue que je trouve cela un peu poussif.

    Notamment lorsque vous dites « Notre code est toujours fortement couplé avec une entité de User (et si je veux enregistrer autre chose qu’une instance de cette classe?)… et le contrôleur peut encore être allégé. »

    Finalement le Handler Register Form que vous faites juste après est lui aussi  » fortement couplé avec une entité de User ». C’est là que cela devient flou pour moi. Peut-être pourriez-vous me donner un cas d’utilisation en exemple ?

    En tous cas, félicitation pour vos articles très élaborés !

    1. Bonjour Jonathan,

      J’ai observé cette pratique de créer un handlerForm dans différents projets, et la première fois, cela m’a aussi interpelé.

      Mais au final, les avantages sont multiples:
      – je ne fais pas de traitement dans le contrôleur mais dans le handlerForm (en faisant appel au userManager, mais je pourrais passer d’autres dépendances qui joueraient un rôle ici)
      – j’injecte dans le service du formHandler le UserManager, et je découple donc le contrôleur du UserManager (et donc du User). Je m’explique: ce formHandler service a un UserManager injecté dans son constructeur, et ce UserManager traite une entité qui implémente l’interface UserInterface.

      Donc, quelque part, le UserManager est déjà découplé de l’entité User car il traite toute entité qui implémente l’interface UserInterface (je viens de changer cette petite donnée dans mon article, c’était une coquille)
      Mais imaginons qu’un jour, je décide de changer ce service est de lui injecter un autre UserManager qui prendra en compte un autre type d’entité User.

      Dans le cas où je n’aurai pas créé de FormHandler, je vais être obligé de revenir dans tous mes contrôleurs et de mettre quelque chose du style:
      $this->container->get(‘app.advanced_user.manager’)->createUser($form->getData()->getUser());

      Et ça m’ennuie, je ne suis pas censé revenir sur du code existant et le modifier. Mais voilà, mon UserManager traite des entités implémentant le UserInterface, et j’utilise peut-être ce code ailleurs, je ne veux pas non plus le modifier. Et mon but ici, est que ce nouveau manager puisse créer un nouveau type de User qui implémentera l’interface UserInterface, mais peut-être également une autre interface, pour lui rajouter des fonctions.

      Il me suffit que:
      – créer un UserAdvancedManager qui implémentera la UserManagerInterface
      – modifier mon service RegisterFormHandler en injectant non plus le UserManager mais le UserAdvancedManager

      Du coup, je ne modifie ni mes contrôleurs, ni mon service RegisterFormHandler (je parle au niveau de l’algorithme). Juste pour l’enregistrement dans cette partie du code, mon service RegisterFormHandler aura un UserAdvancedManager injecté qui, lui-même, créera un autre type de User (AdvancedUser, par exemple)

      Le principe d’encapsuler du code et d’injecter des services qui implémentent une interface permet de découpler le code.

      En laissant le traitement dans le contrôleur, je suis bloqué: je dois revenir à de nombreux endroits, modifier mes tests unitaires, fonctionnels, etc.

      Là, je modifie simplement l’injection de mon service (si ce service n’était utilisé qu’à un endroit bien sûr, sinon il faut créer un autre service RegisterFormHandler, mais c’est un autre débat), et par injection, le nouveau UserManager va pouvoir agir sur un autre type de User.

      1. Merci pour votre réponse très détaillée !

        J’ai été au bout de vos 5 articles, et après avoir répété plusieurs fois ce principe découplage (inscription, changement de mot de passe, mot de passe perdu,…), cela me semble déjà plus naturel.

        Je vois désormais clairement l’intérêt de tels manipulation. En effet les contrôleur ne nécessite plus de changement après cela. Il suffit de modifier ou de créer un nouveau service avec une entité implémentant l’interface UserInterface.

        Merci beaucoup pour votre aide et bonne continuation !

      2. Je me permets de revenir sur le sujet après 2 mois d’avancement sur mon projet.

        Vos articles ont été un réel déblocage pour moi. Je ne conçois plus mon code sans délégation via des services, toutes mes entités utilisent un Manager qui leurs est propre, les validations de formulaire passent toutes via un Handler interagissant avec les Managers, les contrôleurs sont très allégés, les formulaires utilisent les Event lorsque cela est nécessaire, …

        Bref, la qualité de mon code a fait un bond en avant ! Bien heureusement, j’avais pas mal de notions en PHP brut et POO, mais avec de la persévérance pour bien comprendre vos articles, je perçois le framework d’une toute autre manière !

        Tous cela pour vous remercier d’avoir partagé votre savoir aussi proprement, rare sont les tutos/articles aussi bien développé et aboutis.

        1. Bonjour Jonathan,

          J’en suis très heureux! Je suis actuellement en mission avec des dev seniors et j’ai vu qu’ils ont également la même pratique: handler de formulaire, qui a un manager d’entité injecté, qui lui-même a un repository de cette entité qui lui est injecté. Ca permet de bien découpler le code et d’éviter de nombreux if elseif.

          Oui, les events, c’est vraiment quelque chose de super, je m’en suis encore servi tout à l’heure pour un tuto que je prépare sur l’upload de fichier facultatif (car une fois que l’on a imbriqué un formulaire d’upload dans un autre formulaire, Symfony veut obligatoirement qu’un upload soit fait, et je me suis un peu arraché les cheveux pour trouver comment faire, mais la solution s’est en partie trouvée dans l’event POST_SUBMIT)

          Pour le moment, le temps me manque, mais je viens de mettre en place un repo (https://github.com/jpsymfony/symfony2-generic-project) dans lequel j’ai mis l’intégralité de mes connaissances symfony2 actuelles.

          J’en ai profité pour épurer le CoreBundle pour qu’il puisse être repris tel quel dans n’importe quel projet. Il ne contient que des classes génériques.

          J’ai tenté une expérience intéressante: gérer le crud avec le design pattern strategy, et ma foi, j’aime bien le principe, je pense que je vais le garder à présent.

          Dès que je vais avoir plus de temps, j’écrirai d’autres articles et je prendrai ce repo comme exemple en permanence car il contient vraiment tout ce que je pourrai expliquer sur ce site dans les prochains mois.

          Merci encore pour votre message et longue vie de développeur!

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *