Remove null values coming from empty collection form item - forms

I'm trying to implement a ManyToMany relation in a form between 2 entities (say, Product and Category to make simpe) and use the method described in the docs with prototype and javascript (http://symfony.com/doc/current/cookbook/form/form_collections.html).
Here is the line from ProductType that create the category collection :
$builder->add('categories', 'collection', array(
'type' => 'entity',
'options' => array(
'class' => 'AppBundle:Category',
'property'=>'name',
'empty_value' => 'Select a category',
'required' => false),
'allow_add' => true,
'allow_delete' => true,
));
When I had a new item, a new select appear set to the empty value 'Select a category'. The problem is that if I don't change the empty value, it is sent to the server and after a $form->bind() my Product object get some null values in the $category ArrayCollection.
I first though to test the value in the setter in Product entity, and add 'by_reference'=>false in the ProductType, but in this case I get an exception stating that null is not an instance of Category.
How can I make sure the empty values are ignored ?

Citing the documentation on 'delete_empty':
If you want to explicitly remove entirely empty collection entries from your form you have to set this option to true
$builder->add('categories', 'collection', array(
'type' => 'entity',
'options' => array(
'class' => 'AppBundle:Category',
'property'=>'name',
'empty_value' => 'Select a category'),
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true
));
Since you use embedded forms, you could run in some issues such as Warning: spl_object_hash() expects parameter 1 to be object, null given when passing empty collections.
Removing required=>false as explained on this answer did not work for me.
A similar issue is referenced here on github and resolved by the PR 9773

I finally found a way to handle that with Event listeners.
This discussion give the meaning of all FormEvents.
In this case, PRE_BIND (replaced by PRE_SUBMIT in 2.1 and later) will allow us to modify the data before it is bind to the Entity.
Looking at the implementation of Form in Symfony source is the only source of information I found on how to use those Events. For PRE_BIND, we see that the form data will be updated by the event data, so we can alter it with $event->setData(...). The following snippet will loop through the data, unset all null values and set it back.
$builder->addEventListener(FormEvents::PRE_BIND, function(FormEvent $event){
$data = $event->getData();
if(isset($data["categories"])) {
foreach($data as $key=>$value) {
if(!isset($value) || $value == "")
unset($data[$key]);
}
$event->setData($data);
});
Hope this can help others !

Since Symfony 3.4 you can pass a closure to delete_empty:
$builder
->add('authors', CollectionType::class, [
'delete_empty' => function ($author) {
return empty($author['firstName']);
},
]);
https://github.com/symfony/symfony/commit/c0d99d13c023f9a5c87338581c2a4a674b78f85f

Related

Provide default data in collection child form

I have a nested form with prototype feature in Symfony 2. Here is the parent form which contains the collection:
$builder
->add('rooms', 'collection', array(
'type' => new RoomForm(),
'allow_add' => true,
'allow_delete' => true,
'data' => array(new RoomForm()),
))
As you can see, no data_class is defined. After the form submission $form->getData() correctly return associative array.
RoomForm is a simple form class composed by two fields:
$builder
->add(
$builder->create('dateAvailabilityStart', 'text', array(
'label' => 'label.from'
)))
->add(
$builder->create('dateAvailabilityEnd', 'text', array(
'label' => 'label.until'
)))
I would like find a way to populate my collection with existing RoomForm (for edit mode) and associate data in correct fields.
Any ideas?
You could do it from within your controller. Given that above form type is named as RoomFormCollection you could do something like this:
// This should be an array
$rooms = ... // Either from database or...
$form = $this->createForm(new RoomFormCollection(), array(
'rooms' => rooms
));
Another thing, 'data' => array(new RoomForm()), is not valid. RoomForm as its name suggests is a form type, not data struct. You should remove it...
Hope this helps...

How can I Validate All Children of a Form Collection?

I have spent a long time without making progress on what seems to be a simple problem. I need to allow the user to add up to three references to a form. Each reference will have a name and phone number (two text inputs). I would like to do some simple validation on each one. I have created my ReferenceType and nested it into the main form with the following (note, I created the PhoneNumber validator and added the Null() just for testing):
$builder->add('references', 'collection', array(
'type' => new ReferenceType(),
'by_reference' => false,
'allow_add' => true,
'label' => false,
'required' => false,
'attr' => array('class' => 'reference'),
'options' => array(
'required' => false,
'label' => false,
),
'cascade_validation' => true,
'constraints' => array(
new Null(),
new PhoneNumber(),
)
));
ReferenceType:
$builder->add('name', 'text', array(
'label' => 'Reference',
'required' => false,
'cascade_validation' => true,
'attr' => array(
'placeholder' => 'Name'
)
));
$builder->add('phone', 'text', array(
'label' => false,
'required' => false,
'attr' => array(
'placeholder' => 'Phone Number'
),
'cascade_validation' => true,
'constraints' => array(
new Length(array('max' => 14)),
new PhoneNumber(),
)
));
All of that is quite simple and straight forward. However, I have been unable to get the form to throw an error for any reason related to those references. I use Propel for my ORM and have a lot of validation which works correctly on the main form. These references also display and function correctly except for validation. They are all mapped to one column (references) and I have tried Propel's type="array" as well as defining my own getter and setter which serializes the array of items. The type=array doesn't work at all with the collection, and serializing it seems to work ok.
I have searched SO and symfony.com docs for an answer but have found nothing that would actually cause either name or phone to throw an error. I have tried adding validation to my validation.yml file without success (3 chars only for testing):
references:
- Valid: ~
- Collection:
fields:
name:
- Length:
max: 3
maxMessage: Please limit your reference's name to 3 characters or less.
I'm probably missing something very obvious here, but I can't seem to grasp what. I have also dug through the Profiler and everything appears correct in there as well.
Could any provide hints on things to look for? It seems that creating a new database just for references is a little overkill (they don't need to be searchable or anything). Propel has some information on collections, but it didn't seem to make a difference. Or am I entirely missing the idea here and perhaps I should implement the fields an entirely different way?
Symfony 2.6
Propel 2

Symfony2: Form throws "Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be an array, object given" on submit

I'm using FOSUserBundle in one of my projects.
I've build a form based on the object Employee (that has manytomany with RoleGroup).
Here is the form (part of it):
$builder->add('groups', 'entity', array(
'class' => 'MMAAuthBundle:RoleGroup',
'choices' => $this->groups,
'property' => 'name',
'label' => 'Groups',
'expanded' => true,
'attr' => array("multiple" => true)
));
When I submit the form, I get this error in the Profiler:
at ErrorHandler ->handle ('4096', 'Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be an array, object given, called in /home/mihai/intranet/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php on line 528 and defined', '/home/mihai/intranet/vendor/doctrine/collections/lib/Doctrine/Common/Collections/ArrayCollection.php', '47', array())
in /home/mihai/intranet/vendor/doctrine/collections/lib/Doctrine/Common/Collections/ArrayCollection.php at line 47
How can I make the form return an ArrayCollection, not a RoleGroup object?
I had exactly this problem before, but now I'm stuck here.
Your form is currently not a multiple form and therefore passes a single RoleGroup object instead of an array of RoleGroup objects to the constructor of the Collection.
multiple is a form-option ... and not an HTML-attribute. Therefore ...
$builder->add('groups', 'entity', array(
// This would only render a multiple="true" inside the fields HTML tag
'attr' => array("multiple" => true)
... should be ...
$builder->add('groups', 'entity', array(
// multiple option not wrapped by attribute is correct
"multiple" => true

Add Entity field type to form with event subscriber class

I am doing something very similar to this cookbook example http://symfony.com/doc/current/cookbook/form/dynamic_form_generation.html#adding-an-event-subscriber-to-a-form-class
The main difference is that my field type is an entity and not a text type.
So my field subscriber preSetData method looks like this:
public function preSetData(DataEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
if(!$data->getIsCategorized()){
$form->add(
$this->factory->createNamed('categories', 'entity', array(
'class' => 'My\PostBundle\Entity\Category',
'property' => 'name',
'multiple' => true,
'label' => 'Category'
)
)
);
}
}
This is giving the following error
Class does not exist
500 Internal Server Error - ReflectionException
If I add the entity directly in my form type with $builder->add('categories, 'entity', array(... it works fine
Is it possible to attach an entity field type to a form using a field event subscriber in this fashion?
I ran into the same problem, and actually it's because the factory->createNamed() method has more argument than the builder->add
The third argument isn't the options array, but a "data" argument.
So here's what you should do :
$form->add(
$this->factory->createNamed('categories', 'entity', null, array(
'class' => 'My\PostBundle\Entity\Category',
'property' => 'name',
'multiple' => true,
'label' => 'Category'
)
)
);
(add null before the options array)
Whether you attach a field in the type or by means of an event listener/subscriber should make no difference. Either you have a small mistake somewhere (likely), or that's a bug, in which case you should submit it to the issue tracker.

Zend: Form validation: value was not found in the haystack error

I have a form with 2 selects. Based on the value of the first select, it updates the values of the second select using AJAX. Doing this makes the form not being valid. So, I made the next change:
$form=$this->getAddTaskForm(); //the form
if(!$form->isValid($_POST)) {
$values=$form->getValues();
//get the options and put them in $options
$assignMilestone=$form->getElement('assignedMilestone');
$assignMilestone->addMultiOptions($options);
}
if($form->isValid($_POST)) {
//save in the database
}else {
//redisplay the form
}
Basically, I check if it is valid and it isn't if the user changed the value of the first select. I get the options that populated the second select and populate the form with them. Then I try to validate it again. However this doesn't work. Anybody can explain why? The same "value was not found in the haystack" is present.
You could try to deactivate the validator:
in your Form.php
$field = $this->createElement('select', 'fieldname');
$field->setLabel('Second SELECT');
$field->setRegisterInArrayValidator(false);
$this->addElement($field);
The third line will deactivate the validator and it should work.
You can also disable the InArray validator using 'disable_inarray_validator' => true:
For example:
$this->add( array(
'name' => 'progressStatus',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'options' => array(
'disable_inarray_validator' => true,
),
));
Additionaly you should add you own InArray Validator in order to protect your db etc.
In Zend Framework 1 it looks like this:
$this->addElement('select', $name, array(
'required' => true,
'label' => 'Choose sth:',
'filters' => array('StringTrim', 'StripTags'),
'multiOptions' => $nestedArrayOptions,
'validators' => array(
array(
'InArray', true, array(
'haystack' => $flatArrayOptionsKeys,
'messages' => array(
Zend_Validate_InArray::NOT_IN_ARRAY => "Value not found"
)
)
)
)
));
Where $nestedArrayOptions is you multiOptions and $flatArrayOptionsKeys contains you all keys.
You may also add options to select element before checking for the form validation. This way you are insured the select value is in range.