Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Data access and security in service layer (Doctrine & ZF)

We recently started using Doctrine 2.2, and parts of Zend Framework 2 in an effort to improve organization, reduce duplication, among other things. Today, I started throwing around ideas for implementing a service layer to act as a intermediary between our controllers and Doctrine entities.

Right now, the majority of our logic resides in the controller. In addition, we use an action helper to test for certain permissions; however, I came up with a new approach after implementing Zend\Di. I started creating entity-specific service models, which use Zend\Di to inject an EntityManager instance, and the current user's permissions.

The controller code is as follows:

class Project_DeleteController extends Webjawns_Controller_Action
{
    public function init()
    {
        $this->_initJsonContext();
    }

    public function indexAction()
    {
        $response = $this->_getAjaxResponse();

        $auditId = (int) $this->_getParam('audit_id');
        if (!$auditId) {
            throw new DomainException('Audit ID required');
        }

        /* @var $auditService Service\Audit */
        $auditService = $this->getDependencyInjector()->get('Service\Audit');

        try {
            $auditService->delete($auditId);
            $response->setStatusSuccess();
        } catch (Webjawns\Exception\SecurityException $e) {
            $this->_noAuth();
        } catch (Webjawns\Exception\Exception $e) {
            $response->setStatusFailure($e->getMessage());
        }

        $response->sendResponse();
    }
}

And an example of one of our service layers. The constructor takes two parameters--one takes the EntityManager, and the other an Entity\UserAccess object--injected by Zend\Di.

namespace Service;

use Webjawns\Service\Doctrine,
    Webjawns\Exception;

class Audit extends AbstractService
{
    public function delete($auditId)
    {
        // Only account admins can delete audits
        if (\Webjawns_Acl::ROLE_ACCT_ADMIN != $this->getUserAccess()->getAccessRole()) {
            throw new Exception\SecurityException('Only account administrators can delete audits');
        }

        $audit = $this->get($auditId);

        if ($audit->getAuditStatus() !== \Entity\Audit::STATUS_IN_PROGRESS) {
            throw new Exception\DomainException('Audits cannot be deleted once submitted for review');
        }

        $em = $this->getEntityManager();
        $em->remove($audit);
        $em->flush();
    }

    /**
     * @param integer $auditId
     * @return \Entity\Audit
     */
    public function get($auditId)
    {
        /* @var $audit \Entity\Audit */
        $audit = $this->getEntityManager()->find('Entity\Audit', $auditId);
        if (null === $audit) {
            throw new Exception\DomainException('Audit not found');
        }

        if ($audit->getAccount()->getAccountId() != $this->getUserAccess()->getAccount()->getAccountId()) {
            throw new Exception\SecurityException('User and audit accounts do not match');
        }

        return $audit;
    }
}
  1. Is this an appropriate pattern to use for what we are trying to accomplish?
  2. Is it good practice to have the permissions validation within the service layer as posted?
  3. As I understand it, view logic still resides in the controller, giving the model flexibility to be used in various contexts (JSON, XML, HTML, etc.). Thoughts?

I'm happy with the way this works so far, but if anyone sees any downside to how we are doing this, please post your thoughts.

like image 744
webjawns.com Avatar asked Apr 27 '12 23:04

webjawns.com


1 Answers

I like what you're doing here, and I think your separation of concerns is good. We are experimenting with taking it one step further, using custom Repositories. So, for example, where a standard model/service method may look like this:

public function findAll($sort = null)
{
    if (!$sort) $sort = array('name' => 'asc');
    return $this->getEm()->getRepository('Application\Entity\PartType')
                ->findAll($sort);

}

... we are adding things that require DQL to the repository, to keep all DQL out of the models, for Example:

public function findAllProducts($sort = null)
{
    if (!$sort) $sort = array('name' => 'asc');
    return $this->getEm()->getRepository('Application\Entity\PartType')
                ->findAllProducts($sort);

}

For the above model, the repository class looks like this:

<?php
namespace Application\Repository;

use Application\Entity\PartType;
use Doctrine\ORM\EntityRepository;

class PartTypeRepository extends EntityRepository
{

    public function findAllProducts($order=NULL)
    {
        return $this->_em->createQuery(
                    "SELECT p FROM Application\Entity\PartType p 
                        WHERE p.productGroup IS NOT NULL 
                        ORDER BY p.name"
               )->getResult();
    }

}

Note that we have simply extended Doctrine\ORM\EntityRepository which means that we don't have to re-define all the standard Doctrine repository methods, but we can override them if need be, and we can add our own custom ones.

So with regard to access control, it gives us the ability to add identity-based constraints or other record-level conditions at a very low level, by accessing the business logic in your services from the Repository. By doing it this way, the services are unaware of the implementation. As long as we are strict about not putting DQL in other parts of the app, we can achieve record-level business constraints for any class that accesses the database through the repository. (Watch out for custom DQL in higher levels of the app).

Example:

    public function findAll($order=NULL)
    {
        // assumes PHP 5.4 for trait to reduce boilerplate locator code
        use authService;

        if($this->hasIdentity()) {
            return $this->_em->createQuery(
                        "SELECT p FROM Application\Entity\PartType p 
                            JOIN p.assignments a 
                            WHERE a.id = " . $this->getIdentity()->getId()
                   )->getResult();
        } else {
            return NULL;
        }
    }
like image 88
dualmon Avatar answered Sep 29 '22 15:09

dualmon