90

My application currently passes data to my form type using the constructor, as recommended in this answer. However the Symfony 2.8 upgrade guide advises that passing a type instance to the createForm function is deprecated:

Passing type instances to Form::add(), FormBuilder::add() and the FormFactory::create*() methods is deprecated and will not be supported anymore in Symfony 3.0. Pass the fully-qualified class name of the type instead.

Before:    
$form = $this->createForm(new MyType());

After:
$form = $this->createForm(MyType::class);

Seeing as I can't pass data through with the fully-qualified class name, is there an alternative?

Jonathan
  • 13,947
  • 17
  • 94
  • 123

4 Answers4

138

This broke some of our forms as well. I fixed it by passing the custom data through the options resolver.

In your form type:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $this->traitChoices = $options['trait_choices'];

    $builder
        ...
        ->add('figure_type', ChoiceType::class, [
            'choices' => $this->traitChoices,
        ])
        ...
    ;
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'trait_choices' => null,
    ]);
}

Then when you create the form in your controller, pass it in as an option instead of in the constructor:

$form = $this->createForm(ProfileEditType::class, $profile, [
    'trait_choices' => $traitChoices,
]);
Tomas Votruba
  • 23,240
  • 9
  • 79
  • 115
sekl
  • 1,829
  • 1
  • 14
  • 14
  • 1
    Glad to hear I'm not the only one! Brilliant solution, thanks. – Jonathan Dec 02 '15 at 20:47
  • 8
    Just came across this issue as well and did a similar solution. I think if the data is required and if you want to do the kind of type hinting that you'd ordinarily do in the constructor definition, you should use the setRequired() and setAllowedTypes() methods for the options resolver in your configureOptions(), instead of setDefaults(). – sarahg Dec 04 '15 at 20:15
  • 2
    That's exactly what you should do. :) – Bernhard Schussek Dec 10 '15 at 15:00
  • 1
    This works fine from a controller, but I have to pass value from another formType (to an embedded form). I can't use createForm. Is there a solution ? Thanks. – Roubi Jan 08 '16 at 21:38
  • 3
    @Roubi you do the same thing, you define an option in the configureOptions method and then pass it when adding a form field. – Bart Wesselink Jan 09 '16 at 11:59
  • @ Bart Wesselink Thank you, I'll try it this way. – Roubi Jan 09 '16 at 15:30
  • Gee... I almost had a stroke when I found out about this change after 6 hours of work in fixing broken code. I wonder what was the brilliant idea behind this change! – tftd Jan 18 '16 at 01:58
  • @tftd Reasons for this change live [here](https://github.com/symfony/symfony/issues/5321). At least since PHP 5.5 you can use the `::class` construct to generate the class name in a way that IDEs can recognise for refactorings and so on. – contrebis May 15 '16 at 23:06
  • @contrebis I never had problems refactoring when using `new MyForm()`. Anyway, I think the new way is a bit more convenient - you can pass options to your form and it has far better autocomplete support for it. – tftd May 16 '16 at 00:02
  • 1
    @tftd Sure, I just meant easier to manage `\MyNamespace\MyClass::class` as opposed to `'MyNamespace\MyClass'`. – contrebis May 16 '16 at 11:35
  • 2
    I'm not happy with this change either. Thanks for the answer though. – Adambean May 19 '16 at 17:55
  • 2
    FormTypes act like factories, they should be stateless. Injecting values through their constructor (other than via the the service tag method) makes it stateful. This way you have 1 uniform way of creating your form type. Options were always meant to be used instead of constructor arguments. This change is great for DX and usability. – Anyone May 19 '16 at 18:37
  • 1
    thank for helpful answer. For me it didn't work with trait_choices - I had an error "The option "my_custom" does not exist. Defined options are....". So I have used "translation_domain" instead of "trait_choices" (because "Defined options" already in defined options.) – Vaha Oct 27 '16 at 15:06
  • Do we also still need the form field in the Class? What should that look like? private $trait_choices; – Acyra Sep 08 '17 at 10:48
7

Here's how to pass the data to an embedded form for anyone using Symfony 3. First do exactly what @sekl outlined above and then do the following:

In your primary FormType

Pass the var to the embedded form using 'entry_options'

->add('your_embedded_field', CollectionType::class, array(
          'entry_type' => YourEntityType::class,
          'entry_options' => array(
            'var' => $this->var
          )))

In your Embedded FormType

Add the option to the optionsResolver

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'Yourbundle\Entity\YourEntity',
        'var' => null
    ));
}

Access the variable in your buildForm function. Remember to set this variable before the builder function. In my case I needed to filter options based on a specific ID.

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $this->var = $options['var'];

    $builder
        ->add('your_field', EntityType::class, array(
          'class' => 'YourBundle:YourClass',
          'query_builder' => function ($er) {
              return $er->createQueryBuilder('u')
                ->join('u.entity', 'up')
                ->where('up.id = :var')
                ->setParameter("var", $this->var);
           }))
     ;
}
mcriecken
  • 3,217
  • 2
  • 20
  • 23
  • To have less confusion - $this->var is your value you want to pass, not necessarily from class variable. – Darius.V Mar 08 '19 at 17:38
6

Here can be used another approach - inject service for retrieve data.

  1. Describe your form as service (cookbook)
  2. Add protected field and constructor to form class
  3. Use injected object for get any data you need

Example:

services:
    app.any.manager:
        class: AppBundle\Service\AnyManager

    form.my.type:
        class: AppBundle\Form\MyType
        arguments: ["@app.any.manager"]
        tags: [ name: form.type ]

<?php

namespace AppBundle\Form;

use AppBundle\Service\AnyManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class MyType extends AbstractType {

    /**
     * @var AnyManager
     */
    protected $manager;

    /**
     * MyType constructor.
     * @param AnyManager $manager
     */
    public function __construct(AnyManager $manager) {
        $this->manager = $manager;
    }

    public function buildForm(FormBuilderInterface $builder, array $options) {
        $choices = $this->manager->getSomeData();

        $builder
            ->add('type', ChoiceType::class, [
                'choices' => $choices
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults([
            'data_class' => 'AppBundle\Entity\MyData'
        ]);
    }

}
Denis
  • 61
  • 1
  • 4
5

In case anyone is using a 'createNamedBuilder' or 'createNamed' functions from form.factory service here's the snippet on how to set and save the data using it. You cannot use the 'data' field (leave that null) and you have to set the passed data/entities as $options value.

I also incorporated @sarahg instructions about using setAllowedTypes() and setRequired() options and it seems to work fine but you first need to define field with setDefined()

Also inside the form if you need the data to be set remember to add it to 'data' field.

In Controller I am using getBlockPrefix as getName was deprecated in 2.8/3.0

Controller:

/*
* @var $builder Symfony\Component\Form\FormBuilderInterface
*/
$formTicket = $this->get('form.factory')->createNamed($tasksPerformedForm->getBlockPrefix(), TaskAddToTicket::class, null, array('ticket'=>$ticket) );

Form:

public function configureOptions(OptionsResolver $resolver)    {
    $resolver->setDefined('ticket');
    $resolver->setRequired('ticket');
    $resolver->addAllowedTypes('ticket', Ticket::class);

    $resolver->setDefaults(array(           
        'translation_domain'=>'AcmeForm',
        'validation_groups'=>array('validation_group_001'),
        'tasks' => null,
        'ticket' => null,
    ));
}

 public function buildForm(FormBuilderInterface $builder, array $options)   {

    $this->setTicket($options['ticket']);
    //This is required to set data inside the form!
    $options['data']['ticket']=$options['ticket'];

    $builder

        ->add('ticket',  HiddenType::class, array(
                'data_class'=>'acme\TicketBundle\Entity\Ticket',
            )
        )
...
}
Ethernal
  • 172
  • 1
  • 12