Search code examples
zend-frameworkdesign-patternsdatamapperdomain-modelzend-acl

Where to implement Zend_ACL with Domain Object and Data Mapper?


Having read many of Matthew Weier O'Phinney's posts regarding implementing ACL into Models, I have been focused on the best way to go about doing this. However, upon further research on best practices with Domain Objects, I understand that these models should not contain any reference to Data Mappers, or any CRUD operation.

Take for example ERM software which maintains inventory and handles shipments to/from companies based on sale and purchase orders. I imagine having a few domains...

  • Company
  • Shipment
  • Order
  • Product
  • Assembly
  • And a few others

Since Companies can have different Types (e.g. Manufacturer, Supplier, Retailer), this information is stored in numerous tables across my database (e.g. companies, types, company_types). Thus, I have a Data Mapper for my Company Domain which uses objects for the Zend_Db_Table instances of each database table.

In my Controller actions, I understand that there should be very little logic. For instance, creating a new Company may go something like this...

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);
}

With this in mind, I feel that it is best to incorporate my ACL checks into the DataMapper and Validation into the Domain Object. My Domain objects all extend off a base abstract class, which overloads PHP's magic __set and __get methods. In the constructor of each Domain Object, I define the properties of the object by populating a $_properties array with keys. This way, my __set method looks something like...

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;
    }
}

All my Data Mapper's save() methods typehint App_Model_DomainObjectAbstract $obj.

Question #1 - Since my Data Mapper will handle all CRUD actions, and the Domain object should really just contain properties specific to that domain, I feel like ACL checks belong in the Data Mapper - is this acceptable?

I was trying to avoid instantiating Data Mapper's in my Controllers, but this seems irrational now that I think I have a better understanding of this design pattern.

Question #2 - Am I over complicating this process, and should I instead write an ACL Plugin that extends Zend_Controller_Plugin_Abstract and handles ACL based on incoming requests in the preDispatch() method?

Thank you very much for your time!


Solution

  • There's a consensus here (carefully read the answer of @teresko) that ACLs would best fit into the Decorator Pattern (a security container).

    If the privileges definitions of your ACL are stored on the database, then you must have a DataMapper to map between the acl definitions on your database and your actual implementation of Zend_Acl object with its resources, roles and privileges.

    Probably you'll not implement with controller decorators due to the ZF 1 nature (a lot of anti-patterns, global state and so on). Instead, you'll be using cross-cutting concerns with a plugin (preDispatch) that checks it for you. So your ACL must be one of the first objects initialized.

    Considering that your ACL definitions are based on the controller and action names, your plug-in will call your AclMapper to get the populated ACL object and then check if the current user is allowed to access the given resource.

    Check this example code:

    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');
            }
        }
    }