在阅读了Matthew Weier O'Phinney关于在模型中实现ACL的许多帖子后,我一直专注于实现这一点的最佳方法。然而,在对域对象的最佳实践进行进一步研究后,我理解这些模型不应包含对数据映射器或任何CRUD操作的任何引用。
以ERM软件为例,该软件根据销售和采购订单维护库存并处理往返公司的货物。我想有几个域名。。。
- 公司
- 装运
- 订单
- 产品
- 装配
- 还有其他一些
由于公司可以有不同的类型(例如制造商、供应商、零售商),这些信息存储在我的数据库中的许多表中(例如公司、类型、company_Types)。因此,我的公司域有一个数据映射器,它为每个数据库表的Zend_Db_Table实例使用对象。
在我的控制器操作中,我理解应该很少有逻辑。例如,创建一家新公司可能会这样。。。
public function createAction()
{
// Receive JSON request from front end
$data = Zend_Json::decode($request);
$companyObj = new App_Model_Company();
$companyObj->populate($data);
$companyMapper = new App_Model_DataMapper_Company();
$companyMapper->save($companyObj);
}
考虑到这一点,我觉得最好将我的ACL检查合并到DataMapper中,并将验证合并到Domain对象中。My Domain对象都扩展了一个基本抽象类,该类重载了PHP神奇的__set
和__get
方法。在每个域对象的构造函数中,我通过用键填充$_properties
数组来定义对象的属性。这样,我的__set
方法看起来像。。。
public function __set($property, $value)
{
$className = __CLASS__;
if(!array_key_exists($property, $this->_properties))
{
throw new Zend_Exception("Class [ $className ] has no property [ $property ]");
}
// @return Zend_Form
$validator = $this->getValidator();
/*
* Validate provided $value against Zend_Form element $property
*/
$this->properties[$property] = $value;
}
}
我的所有数据映射器的save()
方法都typehint App_Model_DomainObjectAbstract $obj
。
问题#1-由于我的数据映射器将处理所有CRUD操作,并且Domain对象实际上应该只包含该域特定的属性,我觉得ACL检查属于数据映射器-这可以接受吗
我试图避免在控制器中实例化数据映射器,但这似乎是不合理的,因为我认为我对这个设计模式有了更好的理解。
问题#2-我是否过度复杂化了这个过程,是否应该编写一个ACL插件来扩展Zend_Controller_Plugin_Abstract
并根据preDispatch()
方法中的传入请求处理ACL
非常感谢您抽出时间!
这里有一个共识(仔细阅读@teresko的答案),ACLs
最适合Decorator Pattern(一个安全容器)。
如果ACL
的权限定义存储在数据库中,则必须有一个DataMapper来在数据库上的acl
定义和具有resources
、roles
和privileges
的Zend_Acl
对象的实际实现之间进行映射。
由于ZF,您可能不会使用控制器装饰器来实现1自然(大量反模式、全局状态等)。相反,您将使用交叉关注点插件(preDispatch)为您检查它。所以你的ACL必须是一个初始化的第一个对象的。
考虑到ACL定义基于controller
和action
名称,插件将调用AclMapper
以获取已填充的ACL
对象,然后检查是否允许当前用户访问给定的资源。
检查此示例代码:
class Admin_Plugin_AccessCheck extends Zend_Controller_Plugin_Abstract
{
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
if($request->getModuleName() != 'admin')
{
return;
}
$auth = Zend_Auth::getInstance();
$action = null;
if(!$auth->hasIdentity())
{
$action = 'login';
}
else
{
/**
* Note that this is not a good practice (singletons).
* But in this case it's avoiding re-loading the Acl from database
* every time you need it. Also, considering that ZF 1 is full of
* singletons, it'll not hurt, I think ;)
* YOU CAN CHANGE THIS LINE TO $aclMapper->getAcl();
*/
$acl = Acl::getInstance();
$resource = $request->getModuleName() . ':' . $request->getControllerName();
$privilege = $request->getActionName();
$identity = $auth->getStorage()->read();
$role = $identity->role_id;
if($acl->has($resource))
{
if(!$acl->isAllowed($role,$resource,$privilege))
{
$action = 'access-denied';
}
}
}
if($action)
{
$request->setControllerName('authentication')
->setActionName($action)
->setModuleName('admin');
}
}
}
@问题#1:不,ACL不属于映射程序内部。牢记关注点的分离。如果您决定将ACL建立在每个对象的基础上,那么上面链接的decorator方法就是您的选择。不过,装饰器可以很好地围绕映射器来实现。考虑ZF1提供的acl结构:resource:您的域实体,例如类名角色:用户角色特权:C-R-U-D
<?php
class SecurityContainer {
/**@var Zend_Acl*/
protected $acl;
/**@var DataMapper */
protected $mapper;
/**@var User|rolename*/
protected $user;
public function __construct($acl, $mapper, $user) {
$this->acl = $acl;
$this->mapper = $mapper;
$this->user = $user;
}
public function __call($method, $entity) {
if (method_exists($this->mapper, $method) {
if ($this->acl->isAllowed($user, get_class($entity), $method) {
$this->mapper->$method($entity);
}
}
}
这导致问题2:这实际上取决于您如何设计应用程序界面。如果每个实体类型的每个CRUD操作都有一个操作,那么您可以简单地通过FrontController插件来实现您的acl,正如ZF1的许多教程所展示的那样。如果你需要一个更细粒度的ACL,比如说,角色GUEST可能会更新一个公司名称,但经理可能会更新整个实体,或者如果你有多个实体被更改的操作,基于实体的方法是更好的方法。
关于您概述的设计的其他一些想法:我认为让实体验证自己不是一个好主意。尝试实现一个由具体验证器验证类型的解决方案。您甚至可以再次使用decorator;)这仍然是一个更清洁的解决方案。
不应该在控制器操作中使用映射器是有原因的。一个是,与数据库解耦的验收测试变得更加困难(不过,这取决于您的实现)。你指着另一个:行动要尽可能简短。有了ACL和验证器,您的操作将变得更大。考虑像@teresko在另一个问题中所说的那样实现服务层。如果你需要的话,这对基于属性的ACL也很有帮助。
希望这对你有所帮助。