Passing Arguments From Controller To Form Type In Symfony 3

This week I have been updating my Pray For News app that I built for my church, and other churches and groups to use. The app is built in Symfony2 and currently is in version 2.7. I am updating the framework to Symfony 3.1 and removing all the deprecations. Most deprecation notices are easy to eliminate and deal with forms.

If you are updating your app and you find a deprecation notice that says:
Passing type instances to FormBuilder::add(), Form::add() or the FormFactory is deprecated since version 2.8 and will not be supported in 3.0. Use the fully-qualified type class name instead.

Likely you are instantiating a new instance of your form like new YourFormType(). This was fine up until version 2.8 and is no longer the way to get an instance of your form. Unless you make your form into a service, which many times I think is a good idea because you can easily inject dependencies.

The deprecated way of instantiating a form

In my case, I was passing dependency arguments to the form’s __construct method from my controller. Below is an example of the old way to do things.

The controller:

$form = $this->createForm(new TeamType($currentOrg), $entity, array(
    'action' => $this->generateUrl('teams_update', array('id' => $entity->getId())),
    'method' => 'PUT',
));

The form type:

class TeamType extends AbstractType
{
    private $organization;

    public function __construct(Organization $currentOrg)
    {
        $this->organization = $currentOrg;
    }
    ...
}

The updated way of instantiating a form

The new and correct way of getting an instance of your form since Symfony 2.8 is to pass the fully qualified name of the form’s class. For example, pass FormType::class or Namespace\YourAppBundle\Form\YourFormType to the createForm method.

You can see the problem if you wanted to inject dependencies to the construct. In my case, I needed to send the user’s current organization entity that they are logged in under.

$form = $this->createForm(TeamType::class, $entity, array(
    'action'     => $this->generateUrl('teams_update', array('id' => $entity->getId())),
    'method'     => 'PUT',
    'currentOrg' => $currentOrg->getId(),
));

So what to do when you can no longer pass dependencies to the construct? You can pass them in the array options to the buildForm method! Do this in the third argument of the createForm method in the controller, like the example above.

Then in your form type, you can grab the arguments, like below.

/**
 * @param FormBuilderInterface $builder
 * @param array $options
 */
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $this->currentOrg = $options['currentOrg'];
    ...
}

You also must declare your options in the setDefaults method of the configureOptions function. Be sure that you are using the new OptionsResolver class and not the old OptionsResolverInterface class. For good measure I am also setting the setRequired method and the setTypes() method as well.

/**
 * @param OptionsResolver $resolver
 */
public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
            'data_class' => 'Namespace\AppBundle\Entity\Team',
            'currentOrg' => 1,
    ]);

    $resolver->setRequired('currentOrg'); // Requires that currentOrg be set by the caller.
    $resolver->setAllowedTypes('currentOrg', 'integer'); // Validates the type(s) of option(s) passed.
}

Easy enough, right? A special thanks to this StackOverflow question for reference. Leave your comments and any additional knowledge or tricks you have for Symfony 3 forms below!

Published: July 14, 2016 6:08 pm Categorized in:

8 Comments

  • David Duymelinck says:

    If you look at the examples in the symfony documentation, https://symfony.com/doc/current/cookbook/form/create_custom_field_type.html#creating-your-field-type-as-a-service, the right way of doing this is not that easy. Certainly when you need an entity in the constructor.

    To make the form type unit testable I would make the constructor as follows:

    public function __constructor($persistenceService,  $repository, $entityId) {
      // service configuration
      // arguments: ['@doctrine', 'AppBundle:Oranization', 0]
      $this->repository = $persistenceService->getRepository($repository);
      $this->entityId = $entityId;
    }
    

    In the buildForm method the code will be:

    public function buildForm(FormBuilderInterface $builder, array $options) {
      $organization = $this->repository->find($this->entityId);
     // rest of the code
    }
    

    The thing that bothers me the most in your example is that typehinting is thrown out of the window. With my code there is always an instance of Organization in the buildForm method.

    .

    • Kegan V. says:

      Hi David, thank you for your comments! I’ve worked on many different projects where making the form type into a service was needed. However, in this case, I disagree that it is the “right” way of doing it. My example here is to show how to do it from the controller, which is likely the majority of use cases. That being said, if I was going to use this form type in an event listener, or form subscriber, or as an embedded collection in another form, or in some other service, then I think it makes sense to go ahead and register the form as a service in the container. Again, for this scenario I think it is overkill to register it as a service.

      Also, I think you mean to say __construct() not __constructor(). In this case, if I were to make this form a service, passing the entire entity manager would not be necessary because you could set the form instance’s organization id easily by creating a private method which would set it.

      Secondly, the form is absolutely unit testable, as it would fail if the ‘currentOrg’ key wasn’t set in the default options.

      $resolver->setDefaults([
          'data_class' => 'Namespace\AppBundle\Entity\Team',
          'currentOrg' => 1, // This has to be set and defaults to "1"
      ]);
      

      That is the beauty of the OptionsResolver component/class. As far as type hinting being “thrown out of the window”, one could implement the setAllowedTypes() method inside the configureOptions function. Please see documentation on that here. I’ve updated the post to reflect this.

  • Abdul Basit says:

    Hi
    I was using symfony 3 in all the forums i found to pass any ‘option’ name form controller, But this is not the case with symfony 3.
    Thanks for this great tutorial I spent lot of time looking for it.

    Keep up the great work.

  • L .Soriano says:

    I was having a terrible headache trying to send data to my form class. Finally I found this site and you saved my day THANKS!

  • Teddy says:

    Thank you. i spent a lots of time on this problem.

Share Your Comments

I value your privacy, your email address will not be published or shared.

This site uses Akismet to reduce spam. Learn how your comment data is processed.