Symfony应用程序中的Doctrine实体和业务逻辑

Kor*_*nik 56 php domain-driven-design symfony doctrine-orm

欢迎任何想法/反馈:)

我遇到了如何在一个大型Symfony2应用程序中处理我的Doctrine2实体的业务逻辑的问题.(抱歉帖子长度)

在阅读了许多博客,食谱和其他资源后,我发现:

  • 实体可能仅用于数据映射持久性("贫血模型"),
  • 控制器必须更小,
  • 域模型必须与持久层分离(实体不知道实体管理器)

好吧,我完全同意它,但是: 在哪里以及如何处理域模型上复杂的业务规则?


一个简单的例子

我们的域名模型:

  • 一个可以使用角色
  • a 角色可以由不同的组使用
  • 一个用户可以属于多个与许多角色,

SQL持久层中,我们可以将这些关系建模为:

在此输入图像描述

我们的具体业务规则:

  • 仅当角色附加到组时,用户才能在组中拥有角色.
  • 如果我们从组G1中分离角色R1,则必须删除具有组G1和角色R1的所有UserRoleAffectation

这是一个非常简单的例子,但我想知道管理这些业务规则的最佳方法.


找到解决方案

1-服务层实现

使用特定的Service类:

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();   
}
Run Code Online (Sandbox Code Playgroud)
  • (+)每个班级/每个业务规则一个服务
  • ( - )API实体不代表域:可以$group->removeRole($role)从此服务调用.
  • ( - )大型应用程序中的服务类太多了?

2 - 在域实体经理中实施

将这些业务逻辑封装在特定的"域实体管理器"中,也称为模型提供者:

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) {...}
}
Run Code Online (Sandbox Code Playgroud)
  • (+)所有业务规则都是集中的
  • ( - )API实体不代表域:可以从服务中调用$ group-> removeRole($ role)...
  • ( - )域管理员成为FAT经理?

3 - 尽可能使用听众

使用symfony和/或Doctrine事件侦听器:

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 ...
   }
Run Code Online (Sandbox Code Playgroud)

4 - 通过扩展实体实现Rich Models

使用Entities作为Domain Models类的子/父类,它封装了许多Domain逻辑.但这个解决方案对我来说似乎更加困惑.


对您而言,管理此业务逻辑的最佳方式是什么,关注更干净,分离,可测试的代码?您的反馈和良好做法?你有具体的例子吗?

主要资源:

Tom*_*sek 5

我发现解决方案 1) 从长远来看是最容易维护的解决方案。解决方案 2 导致臃肿的“管理器”类最终将被分解成更小的块。

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

“大型应用程序中的服务类过多”并不是避免 SRP 的理由。

在领域语言方面,我发现以下代码类似:

$groupRoleService->removeRoleFromGroup($role, $group);
Run Code Online (Sandbox Code Playgroud)

$group->removeRole($role);
Run Code Online (Sandbox Code Playgroud)

同样根据您的描述,从组中删除/添加角色需要许多依赖项(依赖倒置原则),这对于 FAT/臃肿的管理器来说可能很困难。

解决方案 3) 看起来非常类似于 1) - 每个订阅者实际上都是由实体管理器在后台自动触发的服务,在更简单的场景中它可以工作,但是一旦操作(添加/删除角色)需要大量上下文,就会出现麻烦例如。哪个用户执行了操作,从哪个页面或任何其他类型的复杂验证。


Xav*_*ero 5

请参阅此处:Sf2:在实体内使用服务

也许我的回答有帮助.它只是解决了这个问题:如何"解耦"模型与持久性与控制器层.

在你的具体问题中,我会说这里有一个"诡计"......什么是"团体"?它"独自"?还是它与某人有关?

最初,您的Model类可能看起来像这样:

UserManager (service, entry point for all others)

Users
User
Groups
Group
Roles
Role
Run Code Online (Sandbox Code Playgroud)

UserManager将拥有获取模型对象的方法(如该答案中所述,您永远不应该这样做new).在控制器中,您可以这样做:

$userManager = $this->get( 'myproject.user.manager' );
$user = $userManager->getUserById( 33 );
$user->whatever();
Run Code Online (Sandbox Code Playgroud)

那么...... User,正如你所说,可以有角色,可以分配或不分配.

// 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 );
}
Run Code Online (Sandbox Code Playgroud)

我已经简化了,当然你可以通过Id添加,按对象添加等.

但是当你用"自然语言"来思考时......让我们看看...

  1. 我知道爱丽丝属于摄影师.
  2. 我得到了Alice对象.
  3. 我向Alice询问有关这些组的信息.我得到了组摄影师.
  4. 我向摄影师询问角色.

详细了解:

  1. 我知道Alice是用户id = 33而且她在摄影师的小组中.
  2. 我请求Alice通过UserManager $user = $manager->getUserById( 33 );
  3. 我通过Alice访问组摄影师,也许用`$ group = $ user-> getGroupByName('摄影师');
  4. 然后我想看看小组的角色......我该怎么办?
    • 选项1:$ group-> getRoles();
    • 选项2:$ group-> getRolesForUser($ userId);

第二个是多余的,因为我通过爱丽丝得到了这个小组.您可以创建一个GroupSpecificToUser继承自的新类Group.

类似于游戏...什么是游戏?"游戏"作为"国际象棋"一般?或者你和我昨天开始的"国际象棋"的具体"游戏"?

在这种情况下,$user->getGroups()将返回GroupSpecificToUser对象的集合.

GroupSpecificToUser extends Group
{
    User getPointOfViewUser()
    Roles getRoles()
}
Run Code Online (Sandbox Code Playgroud)

第二种方法将允许您封装迟早会出现的许多其他内容:此用户是否可以在此处执行某些操作?你可以查询组的子类:$group->allowedToPost();,$group->allowedToChangeName();,$group->allowedToUploadImage();等.

在任何情况下,您都可以避免创建一个奇怪的类,只是向用户询问这些信息,就像一种$user->getRolesForGroup( $groupId );方法.

模型不是持久层

我喜欢"忘记"设计时的存在.我通常和我的团队(或者我自己,个人项目)坐在一起,在编写任何代码之前花4到6个小时思考.我们在txt doc中编写API.然后迭代它添加,删除方法等.

您示例的可能"起点"API可能包含任何内容的查询,例如三角形:

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.
Run Code Online (Sandbox Code Playgroud)

活动

正如在尖头文章中所说,我也会在模型中抛出事件,

例如,当从组中的用户中删除角色时,我可以在"监听器"中检测到如果那是最后一个管理员,我可以a)取消删除角色,b)允许它并离开组管理员,c)允许它,但从组中的用户等选择新的管理员或任何适合您的策略.

同样,用户可能只属于50个组(如LinkedIn).然后,您可以只抛出一个preAddUserToGroup事件,并且任何捕获器都可以包含禁止当用户想要加入组51时的规则集.

该"规则"可以明确地留在User,Group和Role类之外,并留在更高级别的类中,该类包含用户可以加入或离开组的"规则".

我强烈建议看到另一个答案.

希望能帮到你!

哈维.

  • 真的很好的答案.我喜欢你将模型与实体分开的方式,但是我有一些麻烦要理解如何用可观察的模式将`User(model)`链接到`User(persistable entity)`.`userManager-> getUserById(Id id)`将使用例如`UserRepository(doctrine repository)`返回从'User(entity)`加载的`User(model)`.这是对的吗 ?所以有两个方法"getUserById",一个在管理器中(返回模型),另一个在存储库中(返回实体)?经理如何联系他们? (2认同)
  • 是的,管理器返回模型,存储库返回实体。控制器和视图永远不会看到存储库。“在编码之前想象”的一种方法如下:即使不需要执行以下操作,也可以想象有人要求您的应用程序能够在普通服务器上运行时从数据库和 json 格式的文件存储中工作当应用程序在运行 PHP 但不运行 mysql 的超微型嵌入式系统中运行时。想象一下,在您的 parameters.yml 中有类似 storage:doctrine 或 storage:json 的内容(在下一个味精中继续) (2认同)
  • (继续之前的评论) - 也许,如果您是纯粹主义者,您会发现您需要一个名为“UserLoaderAndSaver”的辅助类,它能够读取和写入不同的输入和输出并理解您的模型。如果您不想过度设计,请让您的模型和实体在没有“加载程序和保护程序”的情况下连接(虽然这有点难看,但可能会起作用),并使您的控制器和视图仅使用您的模型。该原则总是在幕后与 User(model) -ugly- 或 UserLoaderAndSaver(middleware) -nice- 交互,并且从未从控制器或视图中看到。 (2认同)