24

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)

Community
  • 1
  • 1
DonCallisto
  • 29,419
  • 9
  • 72
  • 100
  • What is the purpose of $modified and what is the purpose of the second parameter to getUserById()? – redbirdo Jul 17 '12 at 15:51
  • Well , the domain business logic goes into [model layer](http://stackoverflow.com/a/5864000/727208). Mosley in the [domain objects](http://c2.com/cgi/wiki?DomainObject). – tereško Jul 17 '12 at 16:41
  • @redbirdo : it doesn't matter for the purpose of question. – DonCallisto Jul 18 '12 at 06:54
  • @tereško : so I'm "walking a good road". I've read your answer and I've found that some points fits perfectly with my description above (entities: domain objects and validators, services: act onto that domain objects and Data Mappers (i.e. repository and entity manager)) – DonCallisto Jul 18 '12 at 07:08
  • @DonCallisto I think the second parameter to getUserById() does matter as I was trying to understand whether your UserManager has a well-defined interface (in the context of 'Is the refactor a good one'). At least I would suggest that you avoid exposing the $request object to your UserManager as it's a UI construct. It would be better to extract the id from the $request and pass it to the UserManager. – redbirdo Jul 18 '12 at 17:52
  • @redbirdo second parameter is there for suggest to UserManager when took "id" from request and when to get it from second parameter directly. I know that passing the request, maybe, isn't a good choice as the request is available to controllers and not to other classes (unless you inject them and so on) but took id out of this isn't a "piece of business logic" ? – DonCallisto Jul 19 '12 at 07:02
  • @DonCallisto Extracting the id from the request is not a piece of business logic but rather exactly the sort of thing the Controller is responsible for - extracting data from the view and passing it to the model. – redbirdo Jul 19 '12 at 08:04
  • @redbirdo ok, that's perfectly logic for me. Thank you for point me a little step forward from understand exactly those concepts. You can put that into an answer, so I can accept it. Remember to make it rich of content and concepts. – DonCallisto Jul 19 '12 at 08:10

4 Answers4

4

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.

André
  • 12,497
  • 6
  • 42
  • 44
  • 2
    Thank you for answer to my question. I've read those articles but that give me nothing more that what i've suppose to know :) If you can expand your answer with more details and fit it with my "real" example, I'll be happy (as all the comunity will be) to read, understand and - even - give you a positive feedback. – DonCallisto Jul 19 '12 at 08:14
0

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
{

}
Sebastian Viereck
  • 5,455
  • 53
  • 53
-1

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

Andrea Sprega
  • 2,221
  • 2
  • 29
  • 35
-1

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

John Smith
  • 1,091
  • 9
  • 17
  • Yes I know, this is a very old answer and, btw, your should be a comment: you're not answering to my question. However now I know where to place business logic: where it belong. I mean, if logic is related to an entity, is almost mandatory (at least to me) to keep it INSIDE entity; otherwise I now use a service for it that could split the logic into sub-services in order to keep single principle responsability valid. Cheers! – DonCallisto May 15 '16 at 21:46
  • 2
    the logic should never be placed inside an entity – Sebastian Viereck Jun 11 '18 at 13:56