6

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.

hakre
  • 193,403
  • 52
  • 435
  • 836
webjawns.com
  • 2,300
  • 2
  • 14
  • 34
  • 1
    Just my two pence on the authentication. I don't believe there is a right way and a wrong way, however, I started putting my authentication into the service layer, but then moved it into my controllers. My reasoning was that the service layer is my internal API and I should use my controller layer to expose that to the world therefore it should decide who gets access to what. Also if I want to build any internal tools/scripts etc, I don't need to build authentication into them to use my service layer. – Jamie Sutherland Apr 27 '12 at 23:40
  • Be careful not to mix up authentication and access control. Authentication can (should?) go into the module, before any domain classes are brought into the picture. Only once you have established the user identity. Example: if (!$authService->hasIdentity()) inside of your domain service model. – dualmon Nov 10 '12 at 19:07
  • 1
    @JamieSutherland: I disagree. The services define the business logic; controllers are the bridge between requests and the appropriate business logic. For example, you would only have a single service for ordering a product, but you might have multiple controllers for HTTP requests, API requests, and so forth. If you're concerned with the ACL being to request specific (for example, via HTTP you might be expecting a user session where as you'd expect a secret key for API requests), generalize your ACL implementation to allow for this. – moteutsch Apr 21 '13 at 08:40

1 Answers1

1

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;
        }
    }
dualmon
  • 1,225
  • 1
  • 8
  • 16
  • 1
    I strongly disagree with placing access-control code in the repository. The repository oughtn't know anything about the business logic of the application. It should be concerned only with retrieving data. Business logic is for higher-level services and classes. – moteutsch Apr 21 '13 at 08:33
  • I think it is also a good idea to abstract the repositories from the Doctrine implementation. Use composition (of the default Doctrine repository classes) instead of inheritance. This combined with coding to an interface allows you to easily swap data sources in some or all of your repositories (what I like to call "mappers," to avoid confusion with Doctrine repositories). – moteutsch Apr 21 '13 at 08:36