Doctrines and business logic in a symfony application

Any ideas / feedback are welcome :)

I am having trouble processing business logic around my Doctrine2 objects in a large Symfony2 application . (Sorry for the length of the message)

After reading many blogs, cookbooks and other resources, I found that:

  • Entities can only be used to save data ("Anemic Model"),
  • Controllers need to be slimmer
  • Domain models must be separated from the persistence level (the entity does not know the entity manager)

Well, I completely agree with this, but: where and how to handle complex business rules on domain models?




Simple example

OUR DOMAIN MODELS:

  • a Group may use Roles
  • a Role can be used by various groups
  • A user can belong to many groups with many Roles ,

At the SQL persistence level, we could model these relationships as:

enter image description here

OUR SPECIFIC BUSINESS RULES:

  • A user can have Roles in Groups only if roles are added to the group.
  • If we separate Role R1 from Group G1 , all UserRoleAffectation with Group G1 and Role R1 must be removed.

This is a very simple example, but I would like to best describe these business rules.




solutions found

1- Service Level Implementation

Use a specific service class like:

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 / business rule
  • (-) API objects do not represent a domain: you can call $group->removeRole($role) from this service.
  • (-) Too many service classes in a large application?



2 - Implementation in Domain Entity Managers

Encapsulate this business logic in a specific "domain entity manager", also call the provider model:

 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 rules are centralized business
  • (-) API objects do not represent a domain: you can call $ group-> removeRole ($ role) from the service ...
  • (-) Domain managers become FAT managers?



3 - Use listeners, if 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 expanding objects

Use Entities as a sub / parent class of Domain Models classes that encapsulate a lot of domain logic. But to me it seems more embarrassed.




Is the best way for you to manage this business logic by focusing on a cleaner, decoupled, testable code? Your feedback and best practices? Do you have specific examples?

Key Resources:

+50
php symfony doctrine2 domain-driven-design
Oct 03 '13 at 8:48
source share
5 answers

I find solution 1) as the easiest to support it from a larger perspective. Solution 2 leads to the bloated class "Manager", which will ultimately be broken down into smaller pieces.

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

“Too many classes of service in a large application” is no reason to exclude SRP.

In terms of the domain, I find the following code similar:

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

and

 $group->removeRole($role); 

In addition, from what you described, removing / adding a role from a group requires a lot of dependencies (the principle of dependency inversion), and this can be difficult using the FAT / bloated manager.

Solution 3) looks very similar to 1) - each subscriber actually starts automatically in the background using Entity Manager, and in simpler scenarios it can work, but problems will arise as soon as the action (adding / removing a role) requires a lot context, for example. which user performed the action, from which page or any other type of comprehensive check.

+3
Oct 08 '13 at 3:52
source share

See here: Sf2: using a service inside an object

Maybe my answer here helps. He simply addresses this: how to "separate" the model from the level of resistance to the levels of the controller.

In your specific question, I would say that there is a "trick" ... what is a "group"? It's one"? or is it when it is connected with someone?

Your model classes might initially look like this:

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

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

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

Then ... User , as you say, there may be 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 simplified, of course, you can add Id, add Object, etc.

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

  • I know that Alice belongs to photographers.
  • I get an Alice object.
  • I ask Alice about groups. I get a group of photographers.
  • I ask photographers for roles.

More details:

  • I know that Alice is a user id = 33, and she is in the group of Photographers.
  • I ask Alice to contact UserManager through $user = $manager->getUserById( 33 );
  • I join a group of photographers through Alice, possibly with `$ group = $ user-> getGroupByName ('Photographers');
  • I would like to see the roles of the group ... What should I do?
    • Option 1: $ group-> getRoles ();
    • Option 2: $ group-> getRolesForUser ($ userId);

The second is similar to redundant, as I got the group through Alice. You can create a new GroupSpecificToUser class that inherits from Group .

Like in a game ... what is a game? "Game" like "chess" in general? Or a specific “game” of “chess” with which you and I started yesterday?

In this case, $user->getGroups() will 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 simply request a group subclass: $group->allowedToPost(); , $group->allowedToChangeName(); , $group->allowedToUploadImage(); etc.

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

The model is not a resistance level.

I like to “forget” about design progress. Usually I 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 are writing an API in dxt dxt. Then try adding, removing methods, etc.

A possible "starting point" API for your example may contain queries of any type of 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. 

Developments

As stated in this article, I would also throw away events in the model,

For example, when deleting a role from a user in a group, I could find in the “listener” that if this was the last administrator, I can: a) cancel the removal of the role, b) allow it and leave the group without an administrator, c) allow it, but select a new administrator using users in the group, etc. or any other option suitable for you.

Similarly, perhaps a user can only belong to 50 groups (as on LinkedIn). Then you can simply throw the preAddUserToGroup event, and any spectacle can contain a set of rules prohibiting this when the user wants to join group 51.

This "rule" can explicitly go beyond the class "User", "Group" and "Role" and leave it in a higher level class that contains "rules" with which users can join or leave groups.

I highly recommend looking at a different answer.

Hope to help!

Xavi.

+5
Aug 27 '14 at
source share

As a personal preference, I like to start simple and grow when more business rules apply. As a rule, I prefer the listeners to better approach .

You just

  • add more listeners as business rules evolve ,
  • each of which has a single responsibility ,
  • and you can test these listeners more easily .

Something that will require a lot of mocks / stubs if you have one class of service, for example:

 class SomeService { function someMethod($argA, $argB) { // some logic A. ... // some logic B. ... // feature you want to test. ... // some logic C. ... } } 
+2
Oct 08 '13 at 3:54
source share

I am a supporter of business-oriented objects. The doctrine has come a long way so as not to pollute your model with infrastructure; It uses reflection, so you can change the accessors as you want. The 2 “Doctrines” that can remain in your feature classes are annotations (you can avoid using ArrayCollection ) and ArrayCollection . This is a library outside of Doctrine ORM ( Doctrine/Common ), so there are no problems.

So, sticking to the basics of DDD, entities are indeed the place to host your domain logic. Of course, sometimes this is not enough, then you can add domain services, services without infrastructure problems.

Doctrine repositories are more average: I prefer them to be the only way to query for entities, events if they do not adhere to the template of the source repository, and I would rather delete the generated methods. Adding a manager service to encapsulate all the fetch / save operations of this class was a common practice of Symfony several years ago, I don't like it at all.

In my experience, you might have a lot more problems with the Symfony form component, I don’t know if you use it. They will severely limit your ability to customize the constructor, then you can use named constructors. Adding the PhpDoc @deprecated̀ tag will give your couples some visual feedback; they should not sue the original constructor.

And last but not least, relying too much on events in the Doctrine will ultimately bite you. They have too many technical limitations, and I find them difficult to track. If necessary, I add domain events sent from the controller / command to the Symfony event manager.

0
Jun 24 '16 at 14:13
source share

I would consider using the service layer separately from the entities themselves. Object classes must describe data structures and, ultimately, some other simple calculations. Complicated rules go to services.

While you use services, you can create more decoupled systems, services, etc. You can take advantage of dependency injection and use events (dispatchers and listeners) to exchange data between services that support them loosely coupled.

I say this based on my own experience. In the beginning, I used all the logic inside entity classes (especially when I developed symfony 1.x / doctrine 1.x applications). Until the applications grew, it was very difficult to maintain.

0
Nov 10 '17 at 17:18
source share



All Articles