Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

symfony 2.3 form getData doesn't work in subforms collections

I have a form which contains a collection. So I have:

/* my type */
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
    ->add('name')
    ->add('photos','collection',array(
        'type'=> new PhotoType(),
        'allow_add'=>true));
}

/*Photo Type*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
    ->add('photoname')
    ->add('size')
}

But I want to access the data inside the photo, so I tried inside the PhotoType:

$data = $builder->getData();

But it seems that it doesn't work, even if I am editting the form, so the photo collection has data. Why can't I access to the $builder->getData() in a form called by another?? Because I'm trying not to do and eventListener...

like image 431
Angel Avatar asked Sep 18 '13 11:09

Angel


2 Answers

To understand what is happening here you have to understand data mapping first. When you call

$form->setData(array('photoname' => 'Foobar', 'size' => 500));

the form's data mapper is responsible for taking the given array (or object) and writing the nested values into the fields of the form, i.e. calling

$form->get('photoname')->setData('Foobar');
$form->get('size')->setData(500);

But in your example, you are not dealing with Form, but with FormBuilder objects. FormBuilder is responsible for collecting the configuration of a form and using this information to produce a Form instance. As such, FormBuilder also lets you store the default data for the form. But since it's a simple configuration object only, it will not invoke the data mapper as of yet. For example:

$builder = $factory->createBuilder()
    ->add('photoname')
    ->add('size')
    ->setData(array('photoname' => 'Foobar', 'size' => 500));

print_r($builder->get('photoname')->getData());
print_r($builder->get('size')->getData());

This example will output:

null
null

because data mapping takes place later, when we turn the FormBuilder into a Form instance. We can use this fact to set separate default values for the individual fields:

$builder->add('size', null, array('data' => 100));
// which is equivalent to
$builder->get('size')
    ->setData(100)
    ->setDataLocked(true);

print_r($builder->get('photoname')->getData());
print_r($builder->get('size')->getData());

And the output:

null
100    

Data locking is required to prevent the data mapper from overriding the default data you just stored. This is done automatically if you pass the "data" option.

At last, you will build the form. Now, FormBuilder calls Form::setData() where necessary, which in turn will invoke the data mapper:

$form = $builder->getForm();

// internally, the following methods are called:

// 1) because of the default data configured for the "size" field
$form->get('size')->setData(100);

// 2) because of the default data configured for the main form
$form->setData(array('photoname' => 'Foobar', 'size' => 500));

// 2a) as a result of data mapping
$form->get('photoname')->setData('Foobar');

// 2b) as a result of data mapping (but ignored, because the data was locked)
$form->get('size')->setData(500);
like image 198
Bernhard Schussek Avatar answered Oct 26 '22 03:10

Bernhard Schussek


As Bernhard indicated, listeners are the only way to do this because the data is not available in the sub form yet. I used the eventListener to solve a similar requirement. Below is a simplified version of my code that I hope will be helpful:

I have a parent form for my View entity which has a lot of fields, as well as collection of other forms. One of the sub forms is for an associated entity ViewVersion, which actually needs to load another form collection for a dynamic entity that is the content type associated with the View. This content type could by one of many different types of entities, e.g Article, Profile, etc. So I need to find out what contentType is set in the View data and then find the dynamic path to that bundle, and include that formType.

Once you know how to do it, it's actually easy!

class ViewType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // Basic Fields Here
            // ...
            // ->add('foo', 'text')
            // ...
            // Load a sub form type for an associated entity
            ->add('version', new ViewVersionType())
        ;
    }
}



class ViewVersionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // Basic Fields Here
            // ...
            // ->add('foo', 'text')
            // ...
        ;

        // In order to load the correct associated entity's formType, 
        // I need to get the form data. But it doesn't exist yet.
        // So I need to use an Event Listener
        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
            // Get the current form
            $form = $event->getForm();
            // Get the data for this form (in this case it's the sub form's entity)
            // not the main form's entity
            $viewVersion = $event->getData();
            // Since the variables I need are in the parent entity, I have to fetch that
            $view = $viewVersion->getView();
            // Add the associated sub formType for the Content Type specified by this view
            // create a dynamic path to the formType
            $contentPath = $view->namespace_bundle.'\\Form\\Type\\'.$view->getContentType()->getBundle().'Type';
            // Add this as a sub form type
            $form->add('content', new $contentPath, array(
                'label' => false
            ));
        });

    }
}

That's it. I'm new to Symfony, so the idea of doing everything in an EventListener is foreign to me (and seems unnecessarily complex). But I hope once I understand the framework better, it will seem more intuitive. As this example indicates, it's not that complicated to do it with an Event Listener, you just wrap your code in that closure (or put it in it's own separate function as described in the docs).

I hope that helps someone!

like image 40
Chadwick Meyer Avatar answered Oct 26 '22 02:10

Chadwick Meyer