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:
- Did I, finally, get the essence of symfony2 and {M}VC in general?
- 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)
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.
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
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
{
}
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:
- 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)
- Controller sends that command to the command bus
- The command bus looks up a handler and handles the command
- 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