Symfony Forms Best Practices
The symfony application framework provides a "form" component that is particularly well thought-out. Nevertheless, this component does confuse some developers initially. Here are a few pointers to get you started in the right direction.
Sample code
The code in the action dealing with the form display and/or the form submission in your controller should look like this:
use ...\...\Form\Type\MyEntityFormType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class EntityController extends Controller
{
public function editAction(Request $request, ...)
{
$entity = // get entity, e.g. by using the request query parameters
$form = $this->createForm(
new MyEntityFormType(),
$entity,
array(
'action' => $this->generateUrl(
$request->attributes
->get('_route'),
)
)
);
$form->handleRequest($request);
if ($form->isValid()) {
$entity->save();
Here you can some custom logic, provided that it remains minimal and legitimately belongs to a controller. For example: display a flash message, redirect the user to another page, etc.
In 99% of the cases, that's exactly what your code should look like. Of course, as with all general principles, there are special cases and exceptions. But if you deviate from it, you should be able to explain, even if only to yourself, why you are doing it.
Explanations
In the controller, the action handling the form must remain as minimal as possible. Most, if not all, of the processes related to the business logic must be defined in other classes.
For example:
- Data validation should be handled by a validation configuration file (
Resources\config\validation.yml) or by custom validator classes - Data transformation must be handled by custom data transformers
- Custom processing of entities before or after being saved must be handled by the entity class itself (Propel ORM) or the appropriate entity manager (Doctrine ORM)
- Ancillary processes triggered by a successful (or failed) form submission, such as email notifications, must be handled by custom event listeners
The only acceptable exceptions are:
- Display a flash message to the user
- Redirect the user to another page
- Notify registered listeners
- Other logic that legitimately belongs inside a controller
Principles involved
A few fundamental design patterns are involved here:
Skinny controller
The idea is that the controller should be as simple as possible. The role of a controller is to act as the "glue" between various elements of the application, not to define business logic.
Single responsibility
The Single Responsibility principle holds that an entity (class or method) should do one thing only. A validator validates, a data transformer transforms data, etc. The controller coordinates these entities.
If you examine the source code of the symfony framework itself, you will see this principle applied throughout. The logic is broken up into many small classes. This is a pattern worth following.
Other practical considerations
Using different actions for displaying and for processing the form
A commonly recommended pattern is to create 2 separate actions for the GET and POST requests. The editAction, for example, will be responsible for displaying the form (GET request); while the updateAction action wil be responsible for processing the form (POST request).
This is perfectly legitimate, and does not contradict the patterns mentioned above (in fact, it is perfectly consistent with the SR principle). Nevertheless, I find that since the single editAction is so simple and short, the benefits of splitting it into 2 actions are limited, and may even be counter-productive. In any case, the point is not to follow principles blindly; the point is to understand why such principles make you more productive and your code easier to maintain. Adjust according to the specific circumstances.
Entity creation action
The same considerations apply when writing the action responsible for creating an entity.
I recommend that you always create the entity first, and that you always pass it as the second argument of the controller's createForm method. According to the case, the entity should be initialized by its constructor (better) or inside the controller action (acceptable in some cases, providing you are not trying to sneak in some business logic that should reside somewhere else).
{
$entity = new MyEntity();
// optional
$entity->setProperty($value);
// or
$entity->init();
$form = $this->createForm(
new MyEntityFormType(),
$entity,
array(
'action' => $this->generateUrl(
$request->attributes->get('_route'),
)
)
)
// ...
}
Form options
The controller's createForm accepts a 3rd argument, which are options passed to the form. Use it!
In particular I recommend that you always pass the action option, as in the example above. This option's value is to the url of the page's form element's action attribute. This has several benefits:
- It improves your code's legibility, as the form's target url is written in the same location where the form is defined, without having to look it up in a template.
- It allows you to take advantage of the twig form-related function
%%{{ form(form) }}%%; where applicable, this enables you to generate the markup for your form in a single instruction.
Form rendering
In a next post I will cover the aspects related to the rendering of the form on the page.
Conclusion
Keep your form-related action short and simple. Resist the temptation to include business logic in it. Use the sample code at the top of this post as a pattern to follow, and justify to yourself any deviations from it. Initially you may find this confusing, as you will need to research a number of related features of the symfony framework (validators, data transformers, event listeners, etc.). But this is a good opportunity to learn about these things, and once the initial hurdle is overcome, you'll find that each piece of logic in your code is in its proper place. As you get to know where that proper place is, this will become second nature to you, making you more productive, and your code much more legible and reusable.