A key concept of domain-driven design is to create a rich design that conveys and reflects the jargon of its domain experts (business users). Then, you want your code to become an expression of that domain model. (see "Ubiquitous Language" and "Model-Driven Design" in the DDD Patterns summaries).
When you do this, you will create names for your entities (classes) that reflect how a business user might describe them. Additionally, you will then create methods on those classes that also reflect the domain.
With this in mind, it may be helpful to consider how you think about your "helper" or "utility" classes. Using some of your descriptions, you might have classes and method such as:
product = GetProduct(data.productId);
shoppingCart.add(product);
receipt = customer.Purchase(shoppingCart);
Your Customer.Purchase method might do things like:
creditCard = this.getCreditCart(creditCardNumber);
purchaseNumber = creditCard.Charge(shoppingCart.Total);
I realize these examples are not complete or even fully accurate, but I hope the idea is helpful.
In response to your original question -- Yes, it is OK to have utility an support classes. However, you want to create those support classes and map them to real domain entities. After some work with your users, you will probably be able to create meaningful domain entites that are more than transaction script entities.
Hope this helps. Good luck!
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...
- I know Alice belongs to a Photographers.
- I get Alice object.
- I query Alice about the groups. I get the group Photographers.
- I query Photographers about the roles.
See more in detail:
- I know Alice is user id=33 and she is in the Photographer's group.
- I request Alice to the UserManager via
$user = $manager->getUserById( 33 );
- I acces the group Photographers thru Alice, maybe with `$group = $user->getGroupByName( 'Photographers' );
- 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.
Best Answer
Logic that is part of the domain logic of the User should stay in the user. This may or may not involve injecting the User entity with a service. I think it depends on whether the service is part of the business logic of the User class, and whether doing this adheres to your ubiquitous language.
I would write this:
This related question has discussion you might find helpful.
I'd also advise you to keep the getters and setters as restricted in scope as possible to reduce coupling.