Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Doctrine Entities and business logic in a Symfony application

Any ideas / feedback are welcome :)

I run into a problem in how to handle business logic around my Doctrine2 entities in a big Symfony2 application. (Sorry for the post length)

After reading many blogs, cookbook and others ressources, I find that :

  • Entities might be used only for data mapping persistence ("anemic model"),
  • Controllers must be the more slim possible,
  • Domain models must be decoupled from persistence layer (entity do not know entity manager)

Ok, I'm totally agree with it, but : where and how handle complex bussiness rules on domain models ?


A simple example

OUR DOMAIN MODELS :

  • a Group can use Roles
  • a Role can be used by different Groups
  • a User can belong to many Groups with many Roles,

In a SQL persistence layer, we could modelize these relations as :

enter image description here

OUR SPECIFIC BUSINESS RULES :

  • User can have Roles in Groups only if Roles is attached to the Group.
  • If we detach a Role R1 from a Group G1, all UserRoleAffectation with the Group G1 and Role R1 must be deleted

This is a very simple example, but i'd like to kown the best way(s) to manage these business rules.


Solutions found

1- Implementation in Service Layer

Use a specific Service class as :

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();    } 
  • (+) one service per class / per business rule
  • (-) API entities is not representating to domain : it's possible to call $group->removeRole($role) out from this service.
  • (-) Too many service classes in a big application ?

2 - Implementation in Domain entity Managers

Encapsulate these Business Logic in specific "domain entities manager", also call Model Providers :

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) {...} } 
  • (+) all businness rules are centralized
  • (-) API entities is not representating to domain : it's possible to call $group->removeRole($role) out from service...
  • (-) Domain Managers becomes FAT managers ?

3 - Use Listeners when possible

Use symfony and/or Doctrine event listeners :

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 - Implement Rich Models by extending entities

Use Entities as sub/parent class of Domain Models classes, which encapsulate lot of Domain logic. But this solutions seems more confused for me.


For you, what is the best way(s) to manage this business logic, focusing on the more clean, decoupled, testable code ? Your feedback and good practices ? Have you concrete examples ?

Main Ressources :

  • Symfony managing entities
  • Symfony2/Doctrine, having to put business logic in my controller? And duplicating controller?
  • Extending Doctrine Entity in order to add business logic
  • http://iamproblematic.com/2012/03/12/putting-your-symfony2-controllers-on-a-diet-part-2/
  • http://l3l0.eu/lang/en/2012/04/anemic-domain-model-problem-in-symfony2/
  • https://leanpub.com/a-year-with-symfony
like image 783
Koryonik Avatar asked Oct 03 '13 08:10

Koryonik


People also ask

What is an entity in Symfony?

Well, entity is a type of object that is used to hold data. Each instance of entity holds exactly one row of targeted database table. As for the directories, Symfony2 has some expectations where to find classes - that goes for entities as well.

What is Symfony model?

Symfony's default model component is based on an object/relational mapping layer. Symfony comes bundles with the two most popular PHP ORMs: Propel and Doctrine. In a symfony application, you access data stored in a database and modify it through objects; you never address the database explicitly.


1 Answers

See here: Sf2 : using a service inside an entity

Maybe my answer here helps. It just addresses that: How to "decouple" model vs persistance vs controller layers.

In your specific question, I would say that there is a "trick" here... what is a "group"? It "alone"? or it when it relates to somebody?

Initially your Model classes probably could look like this:

UserManager (service, entry point for all others)  Users User Groups Group Roles Role 

UserManager would have methods for getting the model objects (as said in that answer, you should never do a new). In a controller, you could do this:

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

Then... User, as you say, can have roles, that can be assigned or not.

// 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 ); } 

I have simplified, of course you could add by Id, add by Object, etc.

But when you think this in "natural language"... let's see...

  1. I know Alice belongs to a Photographers.
  2. I get Alice object.
  3. I query Alice about the groups. I get the group Photographers.
  4. I query Photographers about the roles.

See more in detail:

  1. I know Alice is user id=33 and she is in the Photographer's group.
  2. I request Alice to the UserManager via $user = $manager->getUserById( 33 );
  3. I acces the group Photographers thru Alice, maybe with `$group = $user->getGroupByName( 'Photographers' );
  4. I then would like to see the group's roles... What should I do?
    • Option 1: $group->getRoles();
    • Option 2: $group->getRolesForUser( $userId );

The second is like redundant, as I got the group thru Alice. You can create a new class GroupSpecificToUser which inherits from Group.

Similar to a game... what is a game? The "game" as the "chess" in general? Or the specific "game" of "chess" that you and me started yesterday?

In this case $user->getGroups() would return a collection of GroupSpecificToUser objects.

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

This second approach will allow you to encapsulate there many other things that will appear sooner or later: Is this user allowed to do something here? you can just query the group subclass: $group->allowedToPost();, $group->allowedToChangeName();, $group->allowedToUploadImage();, etc.

In any case, you can avoid creating taht weird class and just ask the user about this information, like a $user->getRolesForGroup( $groupId ); approach.

Model is not persistance layer

I like to 'forget' about the peristance when designing. I usually sit with my team (or with myself, for personal projects) and spend 4 or 6 hours just thinking before writing any line of code. We write an API in a txt doc. Then iterate on it adding, removing methods, etc.

A possible "starting point" API for your example could contain queries of anything, like a 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. 

Events

As said in the pointed article, I would also throw events in the model,

For example, when removing a role from a user in a group, I could detect in a "listener" that if that was the last administrator, I can a) cancel the deletion of the role, b) allow it and leave the group without administrator, c) allow it but choose a new admin from with the users in the group, etc or whatever policy is suitable for you.

The same way, maybe a user can only belong to 50 groups (as in LinkedIn). You can then just throw a preAddUserToGroup event and any catcher could contain the ruleset of forbidding that when the user wants to join group 51.

That "rule" can clearly leave outside the User, Group and Role class and leave in a higher level class that contains the "rules" by which users can join or leave groups.

I strongly suggest to see the other answer.

Hope to help!

Xavi.

like image 155
Xavi Montero Avatar answered Oct 11 '22 03:10

Xavi Montero