I have a form which contains a collection. So I have:
/* my type */
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
In a submit or if you are editing, you can access the data when you turn the FormBuilder into a Form instance. And for a collection type you can try this:
...
$form = $formBuilder->getForm();
...
if ($this->getRestMethod() == 'POST') {
$form->handleRequest($this->get('request'));
if ($form->isValid()) {
$formData = $form->getData();
foreach ($formData['photos'] as $key => $collectionRow) {
var_dump($collectionRow['photoname']);
var_dump($collectionRow['size']);
}
}
}
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!
To add to Chadwick Meyer, (in Symfony 4, but probably applies to earlier versions), an event listener is needed to access the data in a collection because many times data hasn't been created yet and/or hasn't been associated or embedded to the collection. However, there are some intricacies associated with actually getting at the data in the collection via the event listener that becomes important in everyday usage.
In your photo form builder, you'll have to include an event listener:
/*Photo Type*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('photoname')
->add('size');
$builder->addEventListener(FormEvents::POST_SET_DATA,
function (FormEvent $event) {
$form = $event->getForm();
// this would be your entity
$photo = $event->getData();
//Do something with the photo data.
}
);
}
However... if you want to do something with it, you need to make sure you test for nulls as the event is triggered multiple times before and after the data is actually created dynamically. For example, if you want to modify the form on the fly like add some sort of submit button:
/*Photo Type*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('photoname')
->add('size')
$builder->addEventListener(FormEvents::POST_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$form = $event->getForm();
// this would be your entity
$photo = $event->getData();
$formModifier($form,$photo);
}
);
$formModifier = function (FormInterface $form, Photo $photo = null) {
if (!empty($photo)){//Critical to do this test to avoid errors and get to events with data
$form->add('submitButton', SubmitType::class, array(
'label' => 'Do Something',
));
}
};
}
Finally, please note that in certain cases, not all the data will be associated with a particular entity until it is actually persisted in the database. For example, if the entity is being newly created, it will not have its id yet which is usually automatically generated by doctrine or similar during persist. Therefore, in order to associate a submit button or similar to this entity in the collection before it is persisted, you'll probably have to make the 'name' field unique or create a separate field for the entity to hold a unique type parameter and generate it in a unique manner prior to persisting in order to associate something like a submit button to the entity during form creation.
In my case did not need the data necessarily when building the form, but when building the view (later). Just next to the buildForm function of my subform type class, I added the buildView function:
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
class MyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$data = $form->getData();
$view->vars['name'] = $data->objproporwhatever;
}
// ...
}
Because buildView is called later, the data is available there. In this example I used it to change the label of the form row of each item in the collection. Check out the list of possible vars.
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);