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

任何想法/反馈都欢迎:)

我碰到了一个问题,就是如何在Symfony2应用程序中 处理我的Doctrine2实体的 业务逻辑 。 (对不起,发文长度)

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

  • 实体可能仅用于数据映射持久性(“贫血模型”),
  • 控制者必须是更加苗条的可能性,
  • 域模型必须从持久层(实体不知道实体pipe理器)

好吧,我完全同意它,但是: 在域模型中处理复杂商业规则的地方和方式?


一个简单的例子

我们的域名模式:

  • 一个可以使用angular色
  • angular色可以由不同的组使用
  • 一个用户可以属于多个angular色

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

在这里输入图像说明

我们的具体业务规则:

  • 只有angular色连接到组时, 用户才可以在组中具有angular色
  • 如果我们从组G1分离angular色R1 ,则必须删除具有组G1和angular色R1的所有UserRoleAffectation

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


find解决scheme

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(); } 
  • (+)每类/每个商业规则一项服务
  • ( – )API实体不代表域:可以从这个服务中调用$group->removeRole($role)
  • ( – )大型应用程序中的服务类太多?

2 – 在域实体pipe理器中的实现

将这些业务逻辑封装在特定的“域实体pipe理器”中,也可以调用模型提供者:

 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)…
  • ( – )域pipe理员成为FATpipe理员?

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 – 通过扩展实体实现丰富的模型

使用实体作为Domain Models类的子/父类,这些类封装了大量的域逻辑。 但是这个解决scheme似乎对我更困惑。


对于你来说,pipe理这个业务逻辑的最好方法是什么,把重点放在更干净,解耦,可testing的代码上? 你的反馈和良好的做法? 你有具体的例子吗?

主要资源:

  • Symfonypipe理实体
  • Symfony2 / Doctrine,必须把我的控制器的业务逻辑? 和复制控制器?
  • 扩展Doctrine实体以添加业务逻辑
  • http://iamproblematic.com/2012/03/12/putting-your-symfony2-controllers-on-a-diet-part-2/
  • http://l3l0.eu/lang/en/2012/04/anemic-domain-model-problem-in-symfony2/
  • https://leanpub.com/a-year-with-symfony

我觉得解决scheme1)是从更长远的angular度来看最容易维护的。 解决scheme2导致臃肿的“经理”类,最终将被分解成更小的块。

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

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

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

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

 $group->removeRole($role); 

另外从你所描述的,从组中删除/添加angular色需要许多依赖(依赖倒置原则),这可能是一个FAT /臃肿的pipe理器很难。

解决scheme3)看起来非常类似于1) – 每个订阅者实际上是由实体pipe理器在后台自动触发的服务,并且在更简单的情况下它可以工作,但是当动作(添加/删除angular色)需要很多上下文例如。 哪个用户执行该操作,从哪个页面或任何其他types的复杂validation。

见这里: Sf2:使用一个实体内的服务

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

在你的具体问题上,我想说这里有一个“窍门”…什么是“团体”? 它“单独”? 或者当它涉及到某人?

最初你的模型类可能看起来像这样:

 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 ,如你所说,可以有angular色,可以分配或不。

 // 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添加,通过Object添加等。

但是,当你用“自然语言”来想这个…让我们来看看…

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

详细信息请参阅:

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

第二个就像多余,因为我通过爱丽丝得到了这个小组。 您可以创build一个从Groupinheritance的新类GroupSpecificToUser

类似于游戏…什么是游戏? “游戏”一般是“象棋”吗? 或者你和我昨天开始的“国际象棋”的具体“游戏”?

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

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

这第二种方法将允许你在那里封装许多迟早会出现的其他事情:这个用户是否允许在这里做一些事情? 你可以查询组的子类: $group->allowedToPost();$group->allowedToChangeName();$group->allowedToUploadImage(); 等等

在任何情况下,你都可以避免创build一个奇怪的类,只是询问用户这个信息,比如$user->getRolesForGroup( $groupId ); 做法。

模型不是持久层

我喜欢在devise时“忘记”自己的距离。 我通常会和我的团队(或者我自己,为了个人项目)坐在一起,在写任何代码之前花上4到6个小时。 我们在txt文档中编写一个API。 然后迭代它添加,删除方法等

你的例子中一个可能的“起点”API可能包含任何东西的查询,比如三angular形:

 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. 

活动

正如有针对性的文章所说,我也会在模型中引发事件,

例如,当从组中的用户中删除angular色时,我可以在“监听者”中检测到,如果这是最后一个pipe理员,我可以a)取消angular色的删除,b)允许并离开组pipe理员,c)允许但是从组中的用户中select一个新的pipe理员,或者任何适合你的策略。

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

这个“规则”显然可以离开用户,组和angular色类别,并离开一个更高级别的类别,其中包含用户可以join或离开组的“规则”。

我强烈build议看到其他答案。

希望能帮到!

哈维。

作为个人偏好,我喜欢从更简单的事情开始,随着更多业务规则的应用而增长。 因此,我倾向于更倾向于倾听者的态度

你刚才

  • 随着业务规则的发展添加更多的监听器
  • 每个人都有单一的责任
  • 你可以更容易地testing这些监听器

如果你有一个单独的服务类,就会需要大量的mock / stubs:

 class SomeService { function someMethod($argA, $argB) { // some logic A. ... // some logic B. ... // feature you want to test. ... // some logic C. ... } } 

我赞成商业意识的实体。 学说长远不会污染你的模型与基础设施的关注; 它使用reflection,所以你可以自由修改访问器,只要你想。 可能留在实体类中的2个“原则”是注释(您可以避免感谢YML映射)和ArrayCollection 。 这是一个图书馆以外的学说( Doctrine/Common ),所以没有问题。

所以,坚持DDD的基础知识,实体是真正把你的域逻辑的地方。 当然,有时这还不够,那么你可以自由地添加域服务 ,服务没有基础设施问题。

原则更为中间:我宁愿保留这些作为查询实体的唯一方法,如果事件没有粘贴到初始存储库模式,我宁愿删除生成的方法。 添加pipe理器服务来封装给定类的所有读取/保存操作是几年前Symfony常见的做法,我不太喜欢。

根据我的经验,您可能会遇到更多与Symfony表单组件有关的问题,我不知道您是否使用它。 他们会serisouly限制你的能力来定制的构造函数,那么你可以使用命名的构造函数。 添加PhpDoc @deprecated̀标记将给你的配对一些视觉反馈,他们不应该起诉原来的构造函数。

最后但并非最不重要的是,过度依赖教义事件最终会咬你。 他们在技术上的限制太多了,而且我发现那些难以跟踪的东西。 在需要时,我将从控制器/命令调度的域事件添加到Symfony事件分派器。

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

只要你使用服务,你可以创build更多的解耦系统,服务等等。 您可以利用dependency injection的优势,并利用事件(调度员和监听者)在服务之间进行通信,使它们保持弱耦合。

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