Thursday, January 28, 2016

How to inject services into Symfony 2 form

Note

This post addresses latest versions of Symfony 2. In Symfony 3 form instantiation changed, but just a little bit, so most concepts of this post will still be valid.

Sometimes you stumble upon the case when there is need to perform some extra logic while creating or handling a form. Best way to do it without getting your code shattered is to inject services into the form.


What is Symfony form?


In fact, all classes that extend AbstractType are not forms - they are form factories. Real form class is created when you call createForm() method inside your controller. Example:

This is a form definition (this is the reason that it is so popular to name Symfony forms as *Type, because they are form types, not forms themselves).


Real form is created when you do this:

As you can see, after calling controller's createForm() method, Form (not FooType) instance will be returned. Under the hood, your controller calls form.factory service, which in the end calls buildForm() method in your FooType to build the form. This description is of course simplified a bit, if you want to go deeper just click over some sample project and check out The Form Component documentation page.



Why do people create *Form classes instead of *Type classes?


This is just a matter of fashion. Some people respect that what they are writing is a form type definition, while rest is used to create Form classes from other frameworks. I fall into the second group.


Injecting things


Okay, enough theory, let's describe three approaches of injecting things into your form factory.

Worth noting

When you get into AbstractType's guts, you will notice that there is no constructor defined. That gives us field to easily inject stuff using constructor injection.

The ugliest: inside controller


This is the simpliest, yet (in my opinion) ugliest way of doing this. Greatest con is that your controller now is responsible of injecting things into things. This is not what controller is supposed to do. This is the container job. Only case when this come handy is when you test out things and have some time booked for further refactoring. There's one advantage though - you are able to pass other things than services:


The proper: defining form as a service 


This is another approach, way better than the first one. This is the way that most of people go. Create your form type, and then define it as a service:

Having this, you can build your form like this:

Pros:

  • cleaner controller
  • container handles injection
  • thanks to tags, form factory knows where to look for form

Cons:

  • you cannot inject non-service stuff in here
  • one form type = one service. This is bad on bigger projects


By the way, there's a guy named Joe Sexton who got deeper into this approach on his blog post.


The efficient: Have your own form factory


This is the way that I prefer doing it. Speaking marketing language, let's start with advantages:
container handles injection

  • you can pass non-service things
  • you can make few factories in single service, which is the desired way for bigger projects
  • you can even pass form.factory service into your factory, so you can return Form instances directly from your service (no need to call createForm() in controller 

Okay, so let's say that we write ticket booking system and we  have those three forms:


All of above take part in one flow: ticket booking. It is natural to create one service that will handle creation of all of them. Let's create a service then:


And this is service definition:


Now, to get the form:

I hope that you will find above stuff helpful. Happy coding!