Use ZF2 controller plugins for application services within an DDD?

149 Views Asked by At

I am currently using DDD (Domain Driven Design) for a new Zend Framework 2 project. Everything works fine but I do have a question regarding the application services.

I understood that application services are located at the application layer and are kind of the entry point to the domain logic. They can access domain services or the repository for example.

I wonder now if it would make sense to implement the application services as controller plugins. In a classical MVC application this controller plugin could handle the results from the called domain services or repositories. Depending on these results they could generate a redirect response or pass data / a form to a ViewModel. If this logic is encapsulated within a plugin my controller only has to call the plugin and return the result of the plugin.

Am I totally wrong about this? Or would you rather keep the logic how to react on results of a domain service or a repository in a controller?

Best Regards,

Ralf

2

There are 2 best solutions below

1
On

Of course it's kind of subjective and people have strong opinions about things like that... so here's mine:

  1. Controller plugins contain code that's universal to any MVC/REST action and business logic is not universal. Plugins are supposed to facilitate the "controlling" of request/response and not the business logic that's down on the model level. Linking it there would make it less reusable, e.g. for console actions. Also it'd make it less probable to use the business logic with some other framework.
  2. It's awkward to test. Injecting controller plugins as controller class constructor parameters would be kinda redundant, since they are already available from the plugin manager injected into AbstractActionController or AbstractRestfulController. Not having the dependencies injected in a obivious/visible way (like trough contructor method) makes it harder to figure out what the controller class actually depends on. Also since all plugins (AbstractPlugin related) depend on controller instance, switching context from http to console (like for a phpunit test) might get problematic. Also testing logic written/made available as controller plugin would sooner or later escalate to including request/response objects in the tests and that's unnecessary complexity.
  3. It's not intuitive. When I hear plugin I think of something small. Not a full-blown business logic code buried under such inconspicuous name. So when I have little time to debug someones code the last thing I need it to things be out of place like that.

Again I'd like to reiterate, that's just my opinion. I've learned that a lot of patterns can crumble under a weird-enough use case, but the above points made sense to me and my team so far.

0
On

As an example for my solution here you can see an controller action:

    public function showAction()
    {
        $service = $this->readProductEntityCommand;
        $service->setId($this->params()->fromRoute('id'));

        try {
            $result = $service->execute();
        } catch (ProductException $e) {
            $this->flashMessenger()->addMessage($e->getMessage());

            return $this->redirect()->toRoute('part3/product');
        }

        return new ViewModel(
            array(
                'productEntity' => $result->getData(),
            )
        );
    }

And here is an example of an application service build as an command object

class ReadProductEntityCommand implements CommandInterface
{
    protected $productRepository;

    protected $id;

    public function __construct(ProductRepositoryInterface $productRepository)
    {
        $this->productRepository = $productRepository;
    }

    public function setId($id)
    {
        $this->id = $id;
    }

    public function execute()
    {
        if (is_null($this->id)) {
            throw new ProductIdInvalidException(
                'Produkt ID wurde nicht angegeben.'
            );
        }

        try {
            $product = $this->productRepository->getProduct(
                new ProductIdCriterion(
                    new ProductId($this->id)
                )
            );
        } catch (\Exception $e) {
            throw new ProductNotFoundException(
                'Es konnten kein Produkt gelesen werden.'
            );
        }

        if ($product === false) {
            throw new ProductNotFoundException('Produkt wurde nicht gefunden.');
        }

        $result = new Result();
        $result->setValid(true);
        $result->setData($product);
        $result->setMessage('Produkt wurde gelesen.');

        return $result;
    }
}

Maybe this helps someone in the future.