64

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 :

Community
  • 1
  • 1
Koryonik
  • 2,728
  • 3
  • 22
  • 27

5 Answers5

6

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.

Community
  • 1
  • 1
Xavi Montero
  • 9,239
  • 7
  • 57
  • 79
  • 3
    Really good answers. I like the way you separate the model from the entity, but i have some troubles to understand how to link the `User (model)` to the `User (persistable entity)` with the observable pattern. `userManager->getUserById(Id id)` will return a `User (model)` loaded from a `User (entity)` using for example a `UserRepository (doctrine repository)`. Is it correct ? So there is 2 methods "getUserById", one in the manager (returning the model), the other one in the repository (returning the entity) ? How does the manager link them ? – aprovent Nov 26 '15 at 14:37
  • 2
    Yes, the manager returns model, and the repository returns entity. The controllers and views never see the repository. One way to "imagine before coding" is the following: Even of no need to do the following, imagine that someone requires that your application is able to work either from a database when running in an normal server and from file-storage in json format when the application is run in a super-tiny-small-embedded system that runs PHP but does not run mysql. Imagine in your parameters.yml there is something like storage:doctrine or storage:json (continued in next msg) – Xavi Montero Dec 12 '15 at 20:24
  • 2
    (continuing from previous comment) - Maybe, if you are purist, you find you need an auxiliary class named "UserLoaderAndSaver" that is able to read and write to the different inputs and outputs and understands your model. If you do not want to overengineer, make your model and entity connect without the "loader and saver" (although that's a bit ugly, but may work), and make your controllers and views ONLY consume your model. The doctrine is always behind scenes either interacting with the User(model) -ugly- or the UserLoaderAndSaver(middleware) -nice- and never seen from controllers or views. – Xavi Montero Dec 12 '15 at 20:28
  • @XaviMontero what is the difference between handing logic in events or in model ? I mean when we handle logic in model and when handle logic in events? – Alireza Rahmani Khalili Jan 16 '21 at 08:33
  • 1
    Hi, @AlirezaRahmaniKhalili thnks for your question. Events are "messages", so you could potentially wonder if to handle logic in Model or in EventHandlers, not Events themselves. That said, in the "pointed article" I mention "pre" and "post" events. Those correspond to "commands" and "events" in the CQRS+ES world, this is why "pre" events can cancel the operation before it is completed (it is analogous to rejecting a command). This said, in the "post" events (which correspond to "events" in CQRS+ES) you never can action "business logic" because (see next message) – Xavi Montero Jan 17 '21 at 22:27
  • ...because (from prev message) the change has already happened. For example I have a model that controls robots in a mall. If the robot has already "Started to move" towards (x,y) at speed (sx, sy), the event will tell whoever is interested that this already happened. So conclusion, in the "events" ("pre-" events in the terminology of the pointed article) you cannot control logic of THIS model. This does not prevent to use listeners to handle OTHER logic OUTSIDE this domain (take DDD for example, same mall, you have robots, plus you have cameras; say that whenever a robot (continues) – Xavi Montero Jan 17 '21 at 22:30
  • when a robot (from previous comment) starts to go to a certain zone, the zone's cameras start recording video. Then you don't have one model. You have 2 models. The "robot model" does know NOTHING about that cameras exists. And if we are pure in DDD, also the cameras model DO NOT KNOW that robots exist. Then you have sort of multiple utilities and tools that connect and bind multiple domains together. One of my favourites are "Process Managers". The bes way to identify a Process Manager is if you can imagine it as a **human in front of a screen**. For example in this (continues) – Xavi Montero Jan 17 '21 at 22:32
  • in this camera activation thing (continues from prev comment) we could hire a person to stare at a monitor 24x7. And tell him "if a robot goes there, press the record button; if a robot leaves the area, press the stop button in the recorder". That is a symptom that this is a "process manager". This logic does NOT belong to the inside of one model or the other model. This logic is the one that "connects" one model to the other so neither the robots know about cameras, neither the cameras know about the robots. This logic is the one that goes into event handlers. (continues) – Xavi Montero Jan 17 '21 at 22:35
  • (continues from prev. comment) - The easiest way to imagine what belongs to what is to imagine that each domain is coded by a different individual company that does not know each other. You need 3 parts, not 2. Part A done by manufacturer A, part B done by manufacturer B, and a third part that "connects" parts A and B. For example Wordpress does not know Stripe (and should not). Stripe does not know Wordpress (and should not). But you can develop a "plugin for wordpress that enables stripe". Put inside wordpress the "logic" of the "publishing model" (articles, posts, authors), (continues) – Xavi Montero Jan 17 '21 at 22:40
  • (contunies from prev comment) - put inside Stripe the "logic" of the "payment system" (credit cards, customers, etc) and put in the plugin the "logic" that connects both (for example "whenever a wordpress user presses the button "give a tip" get $1 from him (via stripe API) and mention him as a supporter in the page (via wordpress API). I hope this is now cleard. For the "pre" events, just it's an old nomenclature. Don't call them pre-events anymore. Call them "command listeners middleware". This acts as a "plugin to YOUR model" allowing extending it (continues). – Xavi Montero Jan 17 '21 at 22:44
  • (conts from prev comment). For "pre" events (= command listeners middleware), the example would be: Imagine the same robots, but we have a new phyiscal motor for the robot that has more power and can carry more wieght but in exchange it cannot exceed a certain speed. We could then write a "new motor plugin for the robot" that listens the commands and "modifies" the old behaviour. If the previous max speed was for example 5 m/s but the new motor limits to 2 m/s, the pre-event could "block" any attempt to set the robot to 3 m/s. This is "part" of your "extended domain model". Hope to help! – Xavi Montero Jan 17 '21 at 22:48
  • Edit: (can't edit it now). In the second comment, when it says "So conclusion, in the "events" ("pre-" events in the terminology of the pointed article)" it should say "post-" events, of course. – Xavi Montero Jan 17 '21 at 22:50
5

I find solution 1) as the easiest one to maintain from longer perspective. Solution 2 leads bloated "Manager" class which will eventually be broken down into smaller chunks.

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

"Too many service classes in a big application" is not a reason to avoid SRP.

In terms of Domain Language, I find the following code similar:

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

and

$group->removeRole($role);

Also from what you described, removing/adding role from group requires many dependencies (dependency inversion principle) and that could be hard with a FAT/bloated manager.

Solution 3) looks very similar to 1) - each subscriber is actually service automatically triggered in background by Entity Manager and in simpler scenarios it can work, but troubles will arise as soon the action (adding/removing role) will require a lot of context eg. which user performed the action, from which page or any other type of complex validation.

Tomas Dermisek
  • 778
  • 1
  • 8
  • 14
  • Thanks for your feedback. From a **DDD approach**, I find `$group->removeRole($role)` more explicit, but seems harder to implements with the Doctrine entities. **Services & listeners** seems often used in code I have read. I often encountered **Manager** classes also, as in FOS Bundles : https://github.com/FriendsOfSymfony/FOSCommentBundle/blob/master/Model/CommentManager.php, or in Vespolina https://github.com/vespolina/commerce/blob/master/lib/Vespolina/Product/Manager/ProductManager.php but their responsabilities VS Repository is still a bit confused for me. – Koryonik Oct 08 '13 at 07:13
3

I'm in favour of business-aware entities. Doctrine goes a long way not to pollute your model with infrastructure concerns; it uses reflection so you are free to modify accessors as you want. The 2 "Doctrine" things that may remain in your entity classes are annotations (you can avoid them thanks to YML or XML mapping), and the ArrayCollection. This is a library outside of Doctrine ORM (̀Doctrine/Common), so no issues there.

So, sticking to the basics of DDD, entities are really the place to put your domain logic. Of course, sometimes this is not enough, then you are free to add domain services, services without infrastructure concerns.

Doctrine repositories are more middle-ground: I prefer to keep those as the only way to query for entities, event if they are not sticking to the initial repository pattern and I would rather remove the generated methods. Adding manager service to encapsulate all fetch/save operations of a given class was a common Symfony practice some years ago, I don't quite like it.

In my experience, you may come with far more issues with Symfony form component, I don't know if you use it. They will seriously limit your ability to customize the constructor; then you may rather use named constructors. Adding the PhpDoc @deprecated̀ annotation will give your pairs some visual feedback that they should not use the original constructor.

Last but not least, relying too much on Doctrine events will eventually bite you. They are too many technical limitations there, plus I find those hard to keep track of. When needed, I add domain events dispatched from the controller/command to Symfony event dispatcher.

Kamafeather
  • 8,663
  • 14
  • 69
  • 99
romaricdrigon
  • 1,497
  • 12
  • 16
2

I would consider using a service layer apart from the entities itself. Entities classes should describe the data structures and eventually some other simple calculations. Complex rules go to services.

As long you use services you can create more decoupled systems, services and so on. You can take the advantage of dependency injection and utilize events (dispatchers and listeners) to do the communication between the services keeping them weakly coupled.

I say that on basis of my own experience. In the beginning I used to put all the logic inside the entities classes (specially when I developed symfony 1.x/doctrine 1.x applications). As long the applications grew they got really hard to maintain.

Omar Alves
  • 763
  • 5
  • 13
1

As a personal preference, I like to start simple and grow as more business rules are applied. As such I tend to favour the listeners approach better.

You just

  • add more listeners as business rules evolve,
  • each having a single responsibility,
  • and you can test these listeners independently easier.

Something that would require lots of mocks/stubs if you have a single service class such as:

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

        // feature you want to test.
        ...

        // some logic C.
        ...
    }
}
Koryonik
  • 2,728
  • 3
  • 22
  • 27
jorrel
  • 19
  • 1