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

时间:2013-10-03 08:48:37

标签: php symfony doctrine-orm domain-driven-design

欢迎任何想法/反馈:)

我在 Symfony2应用程序中围绕 Doctrine2实体处理业务逻辑时遇到了问题。 (抱歉帖子的长度)

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

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

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


一个简单的例子

我们的域名模型:

  
      
  • 群组可以使用角色
  •   
  • 角色可由不同的群组
  • 使用   
  • 用户可以属于许多
  •   

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

enter image description here

我们的特定业务规则:

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

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


找到解决方案

1-服务层实施

使用特定的服务类:

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();   
}
  • (+)每个班级/每个业务规则一项服务
  • ( - )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) {...}
}
  • (+)所有业务规则都是集中的
  • ( - )API实体不代表域名:可以从服务中调用$ group-> removeRole($ role)...
  • ( - )Domain Managers成为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 ...
   }

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

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


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

主要资源:

5 个答案:

答案 0 :(得分:5)

见这里:Sf2 : using a service inside an entity

也许我的答案有帮助。它只是解决了这个问题:如何“解耦”模型与持久性与控制器层的关系。

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

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

UserManager (service, entry point for all others)

Users
User
Groups
Group
Roles
Role

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

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

然后...... 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 );
}

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

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

  1. 我知道爱丽丝属于摄影师。
  2. 我得到了Alice对象。
  3. 我向Alice询问有关这些群组的信息。我得到了摄影师小组。
  4. 我向摄影师询问角色。
  5. 详细了解详情:

    1. 我知道Alice是用户ID = 33而且她在摄影师小组中。
    2. 我通过$user = $manager->getUserById( 33 );
    3. 向Alice发送请求
    4. 我通过Alice访问团体摄影师,也许用`$ group = $ user-> getGroupByName('摄影师');
    5. 然后我希望看到小组的角色......我该怎么办?
      • 选项1:$ group-> getRoles();
      • 选项2:$ group-> getRolesForUser($ userId);
    6. 第二个就像多余,因为我通过爱丽丝得到了这个小组。您可以创建一个继承自GroupSpecificToUser

      的新类Group

      与游戏相似......什么是游戏? “游戏”作为“国际象棋”一般?或者你和我昨天开始的“国际象棋”的特定“游戏”?

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

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

      第二种方法将允许您封装许多其他迟早会出现的内容:此用户是否可以在此处执行某些操作?您只需查询组子类:$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.
      

      <强>事件

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

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

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

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

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

      希望能帮到你!

      哈维。

答案 1 :(得分:3)

我发现解决方案1)是最容易从较长时间维护的解决方案。解决方案2导致臃肿的“经理”类,最终将分解为更小的块。

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

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

就域名语言而言,我发现以下代码类似:

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

$group->removeRole($role);

同样根据您的描述,从组中删除/添加角色需要许多依赖项(依赖项反转原则),而FAT /膨胀管理器可能很难。

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

答案 2 :(得分:2)

作为个人偏好,我喜欢从简单开始,随着更多业务规则的应用而增长。因此,我倾向于支持听众更好地接受

你只是

  • 在业务规则发展时添加更多侦听器
  • 每个人都有单一责任
  • 您可以更轻松地独立测试这些听众

如果您有一个服务类,例如:

,则需要大量的模拟/存根
class SomeService 
{
    function someMethod($argA, $argB)
    {
        // some logic A.
        ... 
        // some logic B.
        ...

        // feature you want to test.
        ...

        // some logic C.
        ...
    }
}

答案 3 :(得分:0)

我赞成业务感知实体。理论在很大程度上不会因基础设施问题而污染您的模型;它使用反射,因此您可以随意修改访问器。 可能保留在实体类中的2个“Doctrine”事物是注释(您可以避免感谢YML映射)和ArrayCollection。这是Doctrine ORM(Doctrine/Common)之外的一个库,所以没有问题。

因此,坚持DDD的基础知识,实体确实是放置域逻辑的地方。当然,有时这还不够,那么您可以自由添加域名服务,服务没有基础设施问题。

Doctrine 存储库更具中间性:我更喜欢将这些作为查询实体的唯一方法,如果事件不符合初始存储库模式,我宁愿删除生成的方法。添加管理器服务来封装给定类的所有提取/保存操作是几年前常见的Symfony实践,我不太喜欢它。

根据我的经验,您可能会遇到更多Symfony表单组件的问题,我不知道您是否使用它。它们将严格限制您自定义构造函数的能力,然后您可能宁愿使用命名构造函数。添加PhpDoc @deprecated̀标签会给你的对提供一些视觉反馈,他们不应该起诉原始的构造函数。

最后但同样重要的是,过分依赖Doctrine事件最终会让你感到害怕。它们有太多的技术限制,而且我发现很难跟踪。在需要时,我将从控制器/命令调度的域事件添加到Symfony事件调度程序。

答案 4 :(得分:0)

我会考虑使用除实体本身之外的服务层。实体类应该描述数据结构并最终描述一些其他简单的计算。复杂的规则归于服务。

只要您使用服务,您就可以创建更多分离的系统,服务等。您可以利用依赖注入的优势并利用事件(调度程序和侦听器)来完成服务之间的通信,使它们保持弱耦合。

我根据自己的经验说。在开始时,我曾经将所有逻辑放在实体类中(特别是当我开发symfony 1.x / doctrine 1.x应用程序时)。只要应用程序增长,它们就很难维护。