Where to place business logic in Symfony2?

2019-03-12 02:05发布

问题:

After reading a lot of posts and Stack Overflow resources, I've still got some problems about the famous question about "where to put business logic?" Reading StackOverflow Question and A Blog Post, I believe I've understood the issue of code separation well.

Suppose I have a web form where you can add a user that will be added to a db. This example involves these concepts:

  • Form
  • Controller
  • Entity
  • Service
  • Repository

If I didn't miss something, you have to create an entity with some properties, getters, setters and so on in order to make it persist into a db. If you want to fetch or write that entity, you'll use entityManager and, for "non-canonical" query, entityRepository (that is where you can fit your "query language" query).

Now you have to define a service (that is a PHP class with a "lazy" instance) for all business logic; this is the place to put "heavy" code. Once you've recorded the service into your application, you can use it almost everywhere and that involves code reuse and so on.

When you render and post a form, you bind it with your entity (and with constraints of course) and use all the concepts defined above to put all together.

So, "old-me" would write a controller's action in this way:

public function indexAction(Request $request)
    {
        $modified = False;
        if($request->getMethod() == 'POST'){ // submit, so have to modify data
            $em = $this->getDoctrine()->getEntityManager();
            $parameters = $request->request->get('User'); //form retriving
            $id = $parameters['id'];
            $user = $em->getRepository('SestanteUserBundle:User')->find($id);
            $form = $this->createForm(new UserType(), $user);
            $form->bindRequest($request);
            $em->flush();
            $modified = True;
        }

        $users = $this->getDoctrine()->getEntityManager()->getRepository('SestanteUserBundle:User')->findAll();
        return $this->render('SestanteUserBundle:Default:index.html.twig',array('users'=>$users));
    }

"New-me" has refactored code in this way:

   public function indexAction(Request $request)
    {
        $um = $this->get('user_manager');
        $modified = False;
        if($request->getMethod() == 'POST'){ // submit, so have to modify data
            $user = $um->getUserById($request,False);
            $form = $this->createForm(new UserType(), $user);
            $form->bindRequest($request);
            $um->flushAll();
            $modified = True; 
        }
        $users = $um->showAllUser();
        return $this->render('SestanteUserBundle:Default:index.html.twig',array('users'=>$users));
    }

Where $um is a custom service where all code that you can't see from #1 code piece to #2 code piece is stored.

So, here are my questions:

  1. Did I, finally, get the essence of symfony2 and {M}VC in general?
  2. Is the refactor a good one? If not, what would be a better way?

Post Scriptum: I know that I can use the FOSUserBundle for User store and authentication, but this is a basic example for teach myself how to work with Symfony. Moreover, my service was injected with ORM.Doctrine.* in order to work (just a note for who read this question with my same confusion)

回答1:

There are two main approaches regarding on where to put the business logic: the SOA architecture and the domain-driven architecture. If your business objects (entities) are anemic, I mean, if they don’t have business logic, just getters and setters, then you will prefer SOA. However, if you build the business logic inside your business objects, then you will prefer the other. Adam Bien discusses these approaches:

Domain-driven design with Java EE 6: http://www.javaworld.com/javaworld/jw-05-2009/jw-05-domain-driven-design.html

Lean service architectures with Java EE 6: http://www.javaworld.com/javaworld/jw-04-2009/jw-04-lean-soa-with-javaee6.html

It’s Java, but you can get the idea.



回答2:

Is the refactor a good one? If not, what would be a better way?

One of the best framework practices is using param converters to directly invoke an entity from user request.

Example from Symfony documentation:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

/**
 * @Route("/blog/{id}")
 * @ParamConverter("post", class="SensioBlogBundle:Post")
 */
public function showAction(Post $post)
{

}

More on param converters:

http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html



回答3:

Robert C. Martin (the clean code guy) says in his new book clean architecture, that you should put your business logic independently from your framerwork, because the framework will change with time.

So you can put your business logic in a seperate folder like App/Core or App/Manager and avoid inheretence from symfony classes here:

<?php

namespace App\Core;


class UserManager extends BaseManager implements ManagerInterface
{

}


回答4:

I realize this is an old question, but since I had a similar problem I wanted to share my experience, hoping that it might be of help for somebody. My suggestion would be to introduce a command bus and start using the command pattern. The workflow is pretty much like this:

  1. Controller receives request and translates it to a command (a form might be used to do that, and you might need some DTO to move data cleanly from one layer to the other)
  2. Controller sends that command to the command bus
  3. The command bus looks up a handler and handles the command
  4. The controller can then generate the response based on what it needs.

Some code based on your example:

public function indexAction(Request $request)
{
    $command = new CreateUser();
    $form = $this->createForm(new CreateUserFormType(), $command);
    $form->submit($request);
    if ($form->isValid()) {
        $this->get('command_bus')->handle();
    }
    return $this->render(
        'SestanteUserBundle:Default:index.html.twig', 
        ['users' => $this->get('user_manager')->showAllUser()]
    );
}

Then your command handler (which is really part of the service layer) would be responsible of creating the user. This has several advantages:

  • Your controllers are much less likely to become bloated, because they have little to no logic
  • Your business logic is separated from the application (HTTP) logic
  • Your code becomes more testable
  • You can reuse the same command handler but with data coming from a different port (e.g. CLI)

There are also a couple downsides:

  • the number of classes you need in order to apply this pattern is higher and it usually scales linearly with the number of features your application exposes
  • there are more moving pieces and it's a bit harder to reason about, so the learning curve for a team might be a little steeper.

A couple command buses worth noting:

https://github.com/thephpleague/tactician https://github.com/SimpleBus/MessageBus