Entités de Doctrine et logique d'affaires dans une application Symfony

toutes les idées / commentaires sont les bienvenus :)

j'ai rencontré un problème dans la façon de gérer la logique des affaires autour de mon doctrine2 entités dans un grand Symfony2 application . (Désolé pour la longueur du poteau)

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

  • les entités ne peuvent être utilisées que pour le mappage de données de persistance ("anémique modèle"),
  • les Contrôleurs doivent être le plus mince possible,
  • les modèles de domaine doivent être découplés de la couche de persistance (entité ne sait pas Gestionnaire d'entité)

Ok, je suis tout à fait d'accord, mais : où et comment traiter des dossiers complexes d'affaires des règles sur les modèles de domaine ?


un exemple simple

NOS MODÈLES DE DOMAINE:

  • un Groupe peut utiliser des Rôles
  • a rôle peut être utilisé par différents groupes
  • un Utilisateur peuvent appartenir à plusieurs Groupes avec beaucoup de Rôles ,

dans une couche de persistance SQL , nous pourrions modéliser ces relations comme suit:

enter image description here

NOS RÈGLES COMMERCIALES SPÉCIFIQUES:

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

c'est un exemple très simple, mais j'aimerais connaître la meilleure façon de gérer ces règles d'affaires.


Solutions trouvées

1 - mise en Œuvre en Couche de 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 d'affaires
  • ( - ) les entités API ne représentent pas le domaine : il est possible d'appeler $group->removeRole($role) depuis ce service.
  • ( - ) trop de classes de service dans une grande application ?

2 - mise en Œuvre dans le Domaine Gestionnaires de l'entité

Encapsuler ces Logiques de "spécifiques à un domaine d'entités manager", appelé aussi Modèle des Fournisseurs :

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 relatives aux entreprises sont centralisées
  • (-) les entités API ne représentent pas le domaine : il est possible d'appeler $group - >removeRole ($role) out from service...
  • ( - ) les gestionnaires de domaine deviennent des gestionnaires de FAT ?

3 - Utiliser les auditeurs lorsque possible

Utilisation de symfony et/ou de la Doctrine des écouteurs d'événement :

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 des entités comme classe inférieure / mère de Modèles de domaine classes, qui encapsulent beaucoup de logique de domaine. Mais cette solution me semble plus confuse.


pour vous, quelle(s) est (sont) la (Les) meilleure (s) façon (s) de gérer cette logique d'entreprise, en se concentrant sur le code plus propre, découplé, testable ? Vos commentaires et bonnes pratiques ? Avez-vous des exemples concrets ?

Ressources Principales:

53
demandé sur Community 2013-10-03 12:48:36

5 réponses

je trouve la solution 1) comme la plus facile à maintenir d'une perspective plus longue. La Solution 2 conduit à une classe de "Manager" gonflée qui sera éventuellement décomposée en plus petits morceaux.

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

"trop de classes de service dans une grande application" n'est pas une raison pour éviter SRP.

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

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

et

$group->removeRole($role);

aussi à partir de ce que vous avez décrit, supprimer/ajouter le rôle du groupe nécessite de nombreuses dépendances (Principe d'inversion de dépendance) et cela pourrait être difficile avec un gestionnaire FAT/gonflé.

Solution 3) semble très similaire à 1 ) - chaque abonné est en fait un service déclenché automatiquement en arrière-plan par le Gestionnaire D'entité et dans des scénarios plus simples, il peut fonctionner, mais des problèmes surgiront comme bientôt l'action (ajout/suppression de rôle), il faudra beaucoup de contexte par exemple. quel utilisateur a effectué l'action, à partir de quelle page ou tout autre type de validation complexe.

3
répondu Tomas Dermisek 2013-10-08 03:52:00

voir ici: Sf2: utilisation d'un service à l'intérieur d'une entité

Peut-être que ma réponse ici aide. Il s'adresse simplement à cela: comment "découpler" le modèle vs la persistance vs les couches de controller.

Dans votre question, je dirais qu'il y a un "truc" ici... qu'est ce qu'un "groupe"? "Seul"? ou quand il se rapporte à quelqu'un?

initialement, 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 de modèle (comme dit dans cette réponse, vous ne devriez jamais faire un new ). Dans un contrôleur, vous pouvez faire ceci:

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

alors... User , comme vous dites, peut avoir des rôles, qui peut être attribué 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 pouvez ajouter par Id, ajouter par objet, etc.

mais quand on pense à "langage naturel"... voyons voir...

  1. Je sais Qu'Alice appartient à un photographe.
  2. Je reçois objet Alice.
  3. J'interroge Alice sur les groupes. Je reçois le groupe de Photographes.
  4. j'interroge les photographes sur les rôles.

voir plus en détail:

  1. Je sais Qu'Alice est user id=33 et elle est dans le Groupe du photographe.
  2. je demande Alice à L'UserManager via $user = $manager->getUserById( 33 );
  3. j'ai acces au groupe de Photographes thru Alice, peut-être avec `$groupe = $utilisateur->getGroupByName ("Photographes");
  4. j'aimerais voir les rôles du groupe... Que dois-je faire?
    • Option 1: $ group - >getRoles ();
    • Option 2: $ group - > getRolesForUser ($userId);

le second est comme redondant, comme J'ai obtenu le groupe à travers Alice. Vous pouvez créer une nouvelle classe GroupSpecificToUser qui hérite de Group .

similaire à un jeu... qu'est ce qu'un jeu? Le "jeu" que les "échecs" en général? Ou le "jeu" d'Échecs que toi et moi avons commencé hier?

dans ce cas, $user->getGroups() retournerait une collection D'objets GroupSpecificToUser.

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

cette seconde approach vous permettra d'encapsuler beaucoup 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 du groupe: $group->allowedToPost(); , $group->allowedToChangeName(); , $group->allowedToUploadImage(); , etc.

dans tous les cas, vous pouvez éviter de créer taht weird classe et il suffit de demander à l'utilisateur de cette information, comme une $user->getRolesForGroup( $groupId ); approche.

modèle n'est pas couche de persistance

j'aime "d'oublier" la peristance lors de la conception. Je m'assois habituellement avec mon équipe (ou avec moi-même, pour des projets personnels) et passe 4 ou 6 heures à réfléchir avant d'écrire n'importe quelle ligne de code. Nous écrivons une API dans un doc txt. Puis itérez sur elle l'ajout, la suppression des méthodes, etc.

une API" de point de départ "possible 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.

Événements

comme dit dans l'article pointu, je lancerais aussi des événements dans le modèle,

par exemple, en supprimant un rôle d'un utilisateur dans un groupe, je pourrais détecter dans un" auditeur " que si c'était le 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 avec les utilisateurs dans le groupe, etc ou n'importe quelle politique est appropriée pour vous.

De la même façon, peut-être un utilisateur ne peut appartenir qu'à 50 groupes (comme dans LinkedIn). Vous pouvez alors simplement lancer un événement preAddUserToGroup et n'importe quel receveur pourrait contenir la série de règles interdisant cela lorsque l'utilisateur veut rejoindre le groupe 51.

cette "règle" peut clairement sortir de la classe de L'utilisateur, du groupe et du rôle et partir dans une classe supérieure qui contient les "règles" par lesquelles les utilisateurs peuvent rejoindre ou quitter les groupes.

je suggère fortement de voir l'autre réponse.

de l'Espoir pour vous aider!

Xavi.

5
répondu Xavi Montero 2017-05-23 12:00:24

comme préférence personnelle, j'aime commencer simple et de croître que de plus en plus de règles d'affaires sont appliquées. En tant que tel j'ai tendance à favoriser l'approche des auditeurs mieux .

Vous

  • ajouter plus d'auditeurs que les règles métier évoluer ,
  • ayant chacun une responsabilité unique ,
  • et vous pouvez tester ces auditeurs indépendants plus facile.

quelque chose qui exigerait beaucoup de moqueries / talons si vous avez une classe de service unique tel que:

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

        // feature you want to test.
        ...

        // some logic C.
        ...
    }
}
2
répondu jorrel 2013-10-08 06:52:12

je suis en faveur de la affaires entités. La Doctrine va loin pour ne pas polluer votre modèle avec des préoccupations d'infrastructure ; il utilise la réflexion de sorte que vous êtes libre de modifier les accesseurs comme vous voulez. Les 2 choses " Doctrine "qui peuvent rester dans vos classes d'entités sont des annotations (vous pouvez les éviter grâce à la cartographie YML), et le ArrayCollection . Il s'agit d'une bibliothèque en dehors de la Doctrine ORM ( Doctrine/Common ), donc pas de problèmes.

donc, s'en tenir à les bases de DDD, les entités sont vraiment l'endroit pour mettre votre logique de domaine. Bien sûr, parfois cela ne suffit pas , alors vous êtes libre d'ajouter services de domaine , des services sans préoccupations d'infrastructure.

Doctrine référentiels sont plus moyen-sol: je préfère garder ceux que la seule façon de requête pour les entités, même si elles ne sont pas coller à l'initiale d'un modèle de référentiel et je préfère retirer l'généré méthode. Ajouter manager service pour encapsuler toutes les opérations fetch / save d'une classe donnée était une pratique courante de Symfony Il ya quelques années, je ne l'aime pas tout à fait.

dans mon expérience, vous pouvez venir avec beaucoup plus de problèmes avec le composant de forme Symfony, Je ne sais pas si vous l'utilisez. Ils serisouly limiter votre capacité à personnaliser le constructeur, alors vous pouvez plutôt utiliser nommé constructeurs. L'ajout de la balise PhpDoc @deprecated̀ donnera à vos paires un aperçu feedback ils ne devraient pas poursuivre le constructeur d'origine.

enfin, mais non des moindres, trop compter sur les événements de la Doctrine finira par vous mordre. Il y a trop de limites techniques là-bas, et je trouve qu'il est difficile d'en tenir compte. Si nécessaire, j'ajoute domain events envoyé par le controller / command au répartiteur D'événements Symfony.

0
répondu romaricdrigon 2016-06-24 14:13:33

j'envisagerais d'utiliser une couche de service en dehors des entités elles-mêmes. Les classes Entities devraient décrire les structures de données et éventuellement d'autres calculs simples. Les règles complexes vont aux services.

tant que vous utilisez des services, vous pouvez créer plus de Systèmes, services découplés et ainsi de suite. Vous pouvez profiter de l'injection de dépendances et utiliser des événements (répartiteurs et auditeurs) pour faire la communication entre les services en les maintenant faiblement couplés.

je dis cela sur la base de ma propre expérience. Au début j'avais l'habitude de mettre toute la logique dans les classes entities (spécialement quand j'ai développé symfony 1.x / doctrine 1.les applications x). Tant que les applications se sont développées ils sont devenus vraiment difficiles à maintenir.

0
répondu Omar Alves 2017-11-10 17:18:00