Symfony 应用程序中的原则实体和业务逻辑
Posted
技术标签:
【中文标题】Symfony 应用程序中的原则实体和业务逻辑【英文标题】:Doctrine Entities and business logic in a Symfony application 【发布时间】:2013-10-09 21:18:46 【问题描述】:欢迎任何想法/反馈 :)
我在一个大型 Symfony2 应用程序中遇到了如何处理围绕我的 Doctrine2 实体 的业务逻辑 的问题。 (抱歉帖子太长了)
在阅读了许多博客、食谱和其他资源后,我发现:
实体可能仅用于数据映射持久性(“贫血模型”), 控制器必须尽可能纤薄, 域模型必须与持久层解耦(实体不知道实体管理器)好的,我完全同意,但是: 在哪里以及如何处理域模型上的复杂业务规则?
一个简单的例子
我们的领域模型:
组可以使用角色 一个角色可以被不同的组使用 一个用户可以属于许多具有许多角色的组,
在 SQL 持久层中,我们可以将这些关系建模为:
我们的具体业务规则:
用户可以在组中拥有角色仅当角色附加到组时。 如果我们从 Group G1 中分离 Role R1,则必须删除 Group G1 和 Role R1 的所有 UserRoleAffectation
这是一个非常简单的示例,但我想知道管理这些业务规则的最佳方式。
找到的解决方案
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)...
(-) 域管理器变为 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 - 通过扩展实体实现富模型
使用实体作为领域模型类的子/父类,它封装了许多领域逻辑。但是这个解决方案对我来说似乎更困惑。
对您而言,管理此业务逻辑的最佳方式是什么,专注于更干净、解耦、可测试的代码?您的反馈和良好实践?你有具体的例子吗?
主要资源:
Symfony managing entities Symfony2/Doctrine, having to put business logic in my controller? And duplicating controller? Extending Doctrine Entity in order to add business logic 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【问题讨论】:
【参考方案1】:我发现解决方案 1) 从长远来看是最容易维护的。解决方案 2 导致臃肿的“经理”类,最终将被分解成更小的块。
http://c2.com/cgi/wiki?DontNameClassesObjectManagerHandlerOrData
“大型应用程序中有太多的服务类”不是避免 SRP 的理由。
在领域语言方面,我发现以下代码类似:
$groupRoleService->removeRoleFromGroup($role, $group);
和
$group->removeRole($role);
此外,根据您的描述,从组中删除/添加角色需要许多依赖项(依赖项倒置原则),而这对于 FAT/臃肿的管理器可能会很困难。
解决方案 3) 看起来与 1) 非常相似 - 每个订阅者实际上都是由实体管理器在后台自动触发的服务,在更简单的场景中它可以工作,但是一旦操作(添加/删除角色)需要很多上下文,例如。哪个用户执行了操作,从哪个页面或任何其他类型的复杂验证。
【讨论】:
感谢您的反馈。从 DDD 方法,我发现$group->removeRole($role)
更明确,但似乎更难用 Doctrine 实体实现。 Services & listeners 似乎经常在我读过的代码中使用。我也经常遇到 Manager 类,例如在 FOS Bundles 中:github.com/FriendsOfSymfony/FOSCommentBundle/blob/master/Model/…,或在 Vespolina github.com/vespolina/commerce/blob/master/lib/Vespolina/Product/…,但它们的职责 VS Repository 对我来说仍然有点困惑。【参考方案2】:
作为个人喜好,我喜欢从简单开始,随着更多业务规则的应用而成长。因此,我倾向于听众更好地接近。
你只是
随着业务规则的发展添加更多侦听器, 每个人都有单一职责, 您可以更轻松地独立测试这些监听器。如果您只有一个服务类,则需要大量模拟/存根,例如:
class SomeService
function someMethod($argA, $argB)
// some logic A.
...
// some logic B.
...
// feature you want to test.
...
// some logic C.
...
【讨论】:
【参考方案3】:请看这里:Sf2 : using a service inside an entity
也许我在这里的回答会有所帮助。它只是解决了这个问题:如何“解耦”模型、持久性和控制器层。
在您的具体问题中,我想说这里有一个“技巧”......什么是“组”?它“单独”?或者当它与某人有关时?
最初您的模型类可能如下所示:
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添加,按Object添加等等。
但是当你用“自然语言”来思考这个时......让我们看看......
-
我知道 Alice 属于摄影师。
我得到了 Alice 对象。
我向 Alice 询问了有关这些组的信息。我得到了组摄影师。
我向摄影师询问角色。
查看更多详情:
-
我知道 Alice 的用户 id=33,她在摄影师组中。
我通过
$user = $manager->getUserById( 33 );
向 UserManager 请求 Alice
我通过 Alice 访问摄影师组,可能使用 `$group = $user->getGroupByName('Photographers');
然后我想查看组的角色... 我应该怎么做?
选项 1:$group->getRoles();
选项 2:$group->getRolesForUser($userId);
第二个是多余的,因为我通过 Alice 得到了这个组。您可以创建一个新类GroupSpecificToUser
,它继承自Group
。
类似于游戏...什么是游戏?把“游戏”当成“棋”一般?还是你我昨天开始的“棋”的具体“游戏”?
在这种情况下,$user->getGroups()
将返回 GroupSpecificToUser 对象的集合。
GroupSpecificToUser extends Group
User getPointOfViewUser()
Roles getRoles()
第二种方法将允许您封装许多其他迟早会出现的东西:这个用户是否允许在这里做某事?您可以只查询组子类:$group->allowedToPost();
、$group->allowedToChangeName();
、$group->allowedToUploadImage();
等。
在任何情况下,您都可以避免创建非常奇怪的类,而只需向用户询问这些信息,例如 $user->getRolesForGroup( $groupId );
方法。
模型不是持久层
我喜欢在设计时“忘记”持久性。我通常和我的团队(或我自己,对于个人项目)坐在一起,在编写任何代码行之前花 4 或 6 个小时进行思考。我们在 txt 文档中编写 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 (model)
与User (persistable entity)
与可观察模式联系起来。 userManager->getUserById(Id id)
将返回从User (entity)
加载的User (model)
,例如使用UserRepository (doctrine repository)
。这是正确的吗 ?那么有两种方法“getUserById”,一种在管理器中(返回模型),另一种在存储库中(返回实体)?经理如何链接它们?
是的,manager返回model,repository返回entity。控制器和视图永远不会看到存储库。 “在编码之前想象”的一种方法如下:即使不需要执行以下操作,想象有人要求您的应用程序能够在正常服务器中运行时从数据库和 json 格式的文件存储中工作当应用程序在运行 php 但不运行 mysql 的超小型嵌入式系统中运行时。想象一下,在您的 parameters.yml 中有类似 storage:doctrine 或 storage:json 的内容(在下一个 msg 中继续)
(接上一条评论)——也许,如果你是纯粹主义者,你会发现你需要一个名为“UserLoaderAndSaver”的辅助类,它能够读取和写入不同的输入和输出并理解你的模型.如果您不想过度设计,请让您的模型和实体在没有“加载器和保护程序”的情况下连接(虽然这有点难看,但可能有效),并使您的控制器和视图仅使用您的模型。该学说总是在幕后与 User(model) -ugly- 或 UserLoaderAndSaver(middleware) -nice- 交互,并且从未从控制器或视图中看到。
@XaviMontero 在事件或模型中处理逻辑有什么区别?我的意思是什么时候我们在模型中处理逻辑,什么时候在事件中处理逻辑?
嗨,@AlirezaRahmaniKhalili 非常感谢您的提问。事件是“消息”,因此您可能想知道是在模型中还是在事件处理程序中处理逻辑,而不是事件本身。也就是说,在“有针对性的文章”中,我提到了“pre”和“post”事件。这些对应于 CQRS+ES 世界中的“命令”和“事件”,这就是为什么“前”事件可以在操作完成之前取消操作(类似于拒绝命令)。这就是说,在“发布”事件(对应于 CQRS+ES 中的“事件”)中,您永远无法操作“业务逻辑”,因为(请参阅下一条消息)【参考方案4】:
我赞成具有商业意识的实体。 Doctrine 在很大程度上不会因为基础设施问题而污染您的模型;它使用反射,因此您可以随意修改访问器。
可能保留在实体类中的 2 个“原则”是注释(由于 YML 或 XML 映射,您可以避免它们)和 ArrayCollection
。这是 Doctrine ORM 之外的一个库 (̀Doctrine/Common
),所以没有问题。
因此,坚持 DDD 的基础知识,实体确实是放置域逻辑的地方。当然,有时这还不够,那么您可以随意添加域服务,无需考虑基础架构的服务。
Doctrine repositories 更加中立:我更愿意将它们作为查询实体的唯一方法,如果它们不遵守初始存储库模式,我宁愿删除生成的方法。添加 manager 服务来封装给定类的所有获取/保存操作是几年前 Symfony 的常见做法,我不太喜欢它。
根据我的经验,您可能会遇到更多 Symfony 表单组件的问题,我不知道您是否使用它。它们会严重限制您自定义构造函数的能力;那么您可能宁愿使用命名构造函数。添加 PhpDoc @deprecated̀
注释将为您的配对提供一些视觉反馈,表明他们不应使用原始构造函数。
最后但并非最不重要的一点是,过度依赖教义事件最终会咬你一口。它们有太多的技术限制,而且我发现那些很难跟踪。需要时,我将从控制器/命令分派的域事件添加到 Symfony 事件分派器。
【讨论】:
【参考方案5】:我会考虑使用实体本身之外的服务层。实体类应该描述数据结构,并最终描述一些其他简单的计算。复杂的规则转到服务。
只要使用服务,就可以创建更多解耦的系统、服务等。您可以利用依赖注入并利用事件(调度程序和侦听器)在服务之间进行通信,从而保持它们的弱耦合。
我是根据我自己的经验这么说的。一开始我把所有的逻辑都放在实体类中(特别是当我开发 symfony 1.x/doctrine 1.x 应用程序时)。只要应用程序增长,它们就很难维护。
【讨论】:
以上是关于Symfony 应用程序中的原则实体和业务逻辑的主要内容,如果未能解决你的问题,请参考以下文章