56 votes

Entités Doctrine et logique métier dans une application Symfony

Toutes les idées et tous les commentaires sont les bienvenus :)

Je rencontre un problème pour savoir comment gérer la logique commerciale autour de mon Entités de la doctrine 2 dans un grand Application Symfony2 . (Désolé pour la longueur du message)

Après avoir lu de nombreux blogs, livres de cuisine et autres ressources, je trouve que :

  • Les entités peuvent être utilisées uniquement pour la persistance de la cartographie des données ("modèle anémique"),
  • Les contrôleurs doivent être les plus fins possibles,
  • Les modèles de domaine doivent être découplés de la couche de persistance (l'entité ne connaît pas le gestionnaire d'entité).

Ok, je suis tout à fait d'accord, mais : où et comment gérer des règles commerciales complexes sur des modèles de domaine ?


Un exemple simple

NOS MODÈLES DE DOMAINE :

  • a Groupe peut utiliser Rôles
  • a Rôle peut être utilisé par différents Groupes
  • a Utilisateur peut appartenir à plusieurs Groupes avec de nombreux Rôles ,

Dans un SQL nous pourrions modéliser ces relations sous la forme de :

enter image description here

NOS RÈGLES DE GESTION SPÉCIFIQUES :

  • Utilisateur peut avoir Rôles en Groupes seulement si Rôles est attaché au Groupe .
  • Si nous détachons un Rôle R1 d'un Groupe G1 , tous UserRoleAffectation avec le groupe G1 et le rôle R1 doit être supprimé

Il s'agit d'un exemple très simple, mais j'aimerais connaître la (les) meilleure(s) façon(s) de gérer ces règles de gestion.


Solutions trouvées

1- Mise en œuvre dans la couche service

Utiliser une classe de service spécifique comme :

class GroupRoleAffectionService {

  function linkRoleToGroup ($role, $group)
  { 
    //... 
  }

  function unlinkRoleToGroup ($role, $group)
  {
    //business logic to find all invalid UserRoleAffectation with these role and group
    ...

    // BL to remove all found UserRoleAffectation OR to throw exception.
    ...

    // detach role  
    $group->removeRole($role)

    //save all handled entities;
    $em->flush();   
}
  • (+) un service par classe / par règle de gestion
  • (-) Les entités de l'API ne sont pas représentatives du domaine : il est possible d'appeler $group->removeRole($role) de ce service.
  • (-) Trop de classes de service dans une grande application ?

2 - Mise en œuvre dans les gestionnaires d'entités de domaine

Encapsuler cette logique d'entreprise dans des "gestionnaires d'entités de domaine" spécifiques, également appelés fournisseurs de modèles :

class GroupManager {

    function create($name){...}

    function remove($group) {...}

    function store($group){...}

    // ...

    function linkRole($group, $role) {...}

    function unlinkRoleToGroup ($group, $role)
    {

    // ... (as in previous service code)
    }

    function otherBusinessRule($params) {...}
}
  • (+) toutes les règles commerciales sont centralisées
  • (-) Les entités de l'API ne sont pas représentatives du domaine : il est possible d'appeler $group->removeRole($role) en dehors du service...
  • (-) Les gestionnaires de domaine deviennent des gestionnaires de FAT ?

3 - Utiliser des auditeurs lorsque c'est possible

Utiliser des écouteurs d'événements symfony et/ou Doctrine :

class CheckUserRoleAffectationEventSubscriber implements EventSubscriber
{
    // listen when a M2M relation between Group and Role is removed
    public function getSubscribedEvents()
    {
        return array(
            'preRemove'
        );
    }

   public function preRemove(LifecycleEventArgs $event)
   {
    // BL here ...
   }

4 - Mettre en œuvre des modèles riches en étendant les entités

Utiliser les entités comme sous-classe/classe parentale des classes de modèles de domaine, qui encapsulent une grande partie de la logique du domaine. Mais cette solution me semble plus confuse.


Pour vous, quelle est la meilleure façon de gérer cette logique métier, en se concentrant sur un code plus propre, découplé et testable ? Vos commentaires et bonnes pratiques ? Vous avez des exemples concrets ?

Principales ressources :

5voto

Xavi Montero Points 1791

Voir ici : Sf2 : utiliser un service à l'intérieur d'une entité

Peut-être que ma réponse sera utile. Elle porte sur ce point : Comment "découpler" les couches de modèle, de persistance et de contrôleur.

En ce qui concerne votre question spécifique, je dirais qu'il y a un "truc" ici... Qu'est-ce qu'un "groupe" ? Est-ce qu'il est "seul" ? ou est-ce qu'il est lié à quelqu'un ?

Au départ, vos classes de modèles pourraient probablement ressembler à ceci :

UserManager (service, entry point for all others)

Users
User
Groups
Group
Roles
Role

UserManager aurait des méthodes pour obtenir les objets du modèle (comme indiqué dans cette réponse, vous ne devriez jamais faire un new ). Dans un contrôleur, vous pourriez faire ceci :

$userManager = $this->get( 'myproject.user.manager' );
$user = $userManager->getUserById( 33 );
$user->whatever();

Puis... User Comme vous le dites, il peut y avoir des rôles, qui peuvent être attribués ou non.

// Using metalanguage similar to C++ to show return datatypes.
User
{
    // Role managing
    Roles getAllRolesTheUserHasInAnyGroup();
    void  addRoleById( Id $roleId, Id $groupId );
    void  removeRoleById( Id $roleId );

    // Group managing
    Groups getGroups();
    void   addGroupById( Id $groupId );
    void   removeGroupById( Id $groupId );
}

J'ai simplifié, bien sûr vous pourriez ajouter par Id, ajouter par Objet, etc.

Mais si l'on raisonne en "langage naturel"... voyons voir...

  1. Je sais qu'Alice appartient à un groupe de photographes.
  2. Je reçois l'objet d'Alice.
  3. J'interroge Alice sur les groupes. J'obtiens le groupe Photographes.
  4. J'interroge les photographes sur leurs rôles.

Voir plus en détail :

  1. Je sais qu'Alice est l'utilisateur id=33 et qu'elle fait partie du groupe des photographes.
  2. Je demande Alice au UserManager par l'intermédiaire de $user = $manager->getUserById( 33 );
  3. J'accède au groupe Photographes par l'intermédiaire d'Alice, peut-être avec `$group = $user->getGroupByName( 'Photographers' ) ;
  4. Je voudrais ensuite voir les rôles du groupe... Que dois-je faire ?
    • Option 1 : $group->getRoles() ;
    • Option 2 : $group->getRolesForUser( $userId ) ;

La seconde est comme redondante, puisque j'ai obtenu le groupe par l'intermédiaire d'Alice. Vous pouvez créer une nouvelle classe GroupSpecificToUser qui hérite de Group .

Semblable à un jeu... qu'est-ce qu'un jeu ? Le "jeu" comme les "échecs" en général ? Ou le "jeu" spécifique d'"échecs" que vous et moi avons commencé hier ?

Dans ce cas $user->getGroups() renverrait une collection d'objets GroupSpecificToUser.

GroupSpecificToUser extends Group
{
    User getPointOfViewUser()
    Roles getRoles()
}

Cette deuxième approche vous permettra d'encapsuler bien d'autres choses qui apparaîtront tôt ou tard : Cet utilisateur est-il autorisé à faire quelque chose ici ? Vous pouvez simplement interroger la sous-classe de groupe : $group->allowedToPost(); , $group->allowedToChangeName(); , $group->allowedToUploadImage(); , etc.

Dans tous les cas, vous pouvez éviter de créer cette classe bizarre et vous contenter de demander ces informations à l'utilisateur, comme une $user->getRolesForGroup( $groupId ); l'approche.

Le modèle n'est pas une couche de persistance

J'aime "oublier" la résistance lors de la conception. J'ai l'habitude de m'asseoir avec mon équipe (ou avec moi-même, pour les projets personnels) et de passer 4 ou 6 heures à réfléchir avant d'écrire la moindre ligne de code. Nous écrivons une API dans un document txt. Puis nous itérons dessus en ajoutant, supprimant des méthodes, etc.

Un "point de départ" possible de l'API pour votre exemple pourrait contenir des requêtes de n'importe quoi, comme un triangle :

User
    getId()
    getName()
    getAllGroups()                     // Returns all the groups to which the user belongs.
    getAllRoles()                      // Returns the list of roles the user has in any possible group.
    getRolesOfACertainGroup( $group )  // Returns the list of groups for which the user has that specific role.
    getGroupsOfRole( $role )           // Returns all the roles the user has in a specific group.
    addRoleToGroup( $group, $role )
    removeRoleFromGroup( $group, $role )
    removeFromGroup()                  // Probably you want to remove the user from a group without having to loop over all the roles.
    // removeRole() ??                 // Maybe you want (or not) remove all admin privileges to this user, no care of what groups.

Group
    getId()
    getName()
    getAllUsers()
    getAllRoles()
    getAllUsersWithRole( $role )
    getAllRolesOfUser( $user )
    addUserWithRole( $user, $role )
    removeUserWithRole( $user, $role )
    removeUser( $user )                 // Probably you want to be able to remove a user completely instead of doing it role by role.
    // removeRole( $role ) ??           // Probably you don't want to be able to remove all the roles at a time (say, remove all admins, and leave the group without any admin)

Roles
    getId()
    getName()
    getAllUsers()                  // All users that have this role in one or another group.
    getAllGroups()                 // All groups for which any user has this role.
    getAllUsersForGroup( $group )  // All users that have this role in the given group.
    getAllGroupsForUser( $user )   // All groups for which the given user is granted that role
    // Querying redundantly is natural, but maybe "adding this user to this group"
    // from the role object is a bit weird, and we already have the add group
    // to the user and its redundant add user to group.
    // Adding it to here maybe is too much.

Evénements

Comme indiqué dans l'article en question, j'ajouterais également des événements dans le modèle,

Par exemple, lorsque je supprime un rôle d'un utilisateur dans un groupe, je peux détecter dans un "auditeur" que s'il s'agit du dernier administrateur, je peux a) annuler la suppression du rôle, b) l'autoriser et laisser le groupe sans administrateur, c) l'autoriser mais choisir un nouvel administrateur parmi les utilisateurs du groupe, etc. ou toute autre politique qui vous convient.

De même, un utilisateur ne peut appartenir qu'à 50 groupes (comme dans LinkedIn). Il suffit alors de lancer un événement preAddUserToGroup et tout catcher peut contenir le jeu de règles interdisant cela lorsque l'utilisateur souhaite rejoindre le groupe 51.

Cette "règle" peut clairement sortir de la classe des utilisateurs, des groupes et des rôles et se retrouver dans une classe de niveau supérieur qui contient les "règles" selon lesquelles les utilisateurs peuvent rejoindre ou quitter des groupes.

Je vous conseille vivement de consulter l'autre réponse.

J'espère pouvoir vous aider !

Xavi.

4voto

Tomas Dermisek Points 473

Je pense que la solution 1) est la plus facile à maintenir à long terme. La solution 2 conduit à une classe "Manager" gonflée qui sera éventuellement décomposée en morceaux plus petits.

http://c2.com/cgi/wiki?DontNameClassesObjectManagerHandlerOrData

"Le fait qu'il y ait trop de classes de service dans une grande application n'est pas une raison pour éviter l'ASR.

En termes de langage de domaine, je trouve le code suivant similaire :

$groupRoleService->removeRoleFromGroup($role, $group);

y

$group->removeRole($role);

De plus, d'après ce que vous avez décrit, la suppression/l'ajout d'un rôle dans un groupe nécessite de nombreuses dépendances (principe d'inversion des dépendances), ce qui peut s'avérer difficile avec un gestionnaire FAT/bloated.

La solution 3) ressemble beaucoup à la solution 1) - chaque abonné est en fait un service déclenché automatiquement en arrière-plan par Entity Manager. Dans les scénarios les plus simples, cela peut fonctionner, mais des problèmes surgiront dès que l'action (ajouter/supprimer un rôle) nécessitera beaucoup de contexte, par exemple quel utilisateur a effectué l'action, à partir de quelle page ou tout autre type de validation complexe.

2voto

jorrel Points 11

Personnellement, j'aime commencer par des règles simples et les développer au fur et à mesure de l'application des règles de gestion. C'est pourquoi j'ai tendance à privilégier l'approche des auditeurs est meilleure .

Vous venez de

  • añada plus d'auditeurs au fur et à mesure de l'évolution des règles de gestion ,
  • chacun ayant un responsabilité unique ,
  • et vous pouvez tester ces auditeurs de manière indépendante plus facile.

Quelque chose qui nécessiterait beaucoup de mocks/stubs si vous avez une seule classe de service telle que :

class SomeService 
{
    function someMethod($argA, $argB)
    {
        // some logic A.
        ... 
        // some logic B.
        ...

        // feature you want to test.
        ...

        // some logic C.
        ...
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X