Symfony2 embedded forms + dynamic form update - forms

I have a dropdown menu in my form and the form structure depends on its value. I have managed to solve the "form-update-issue" with event subscriber/listener class, where i am trying to update the main form according to dropdown's value.
The main problem is that i have to modify the form from values that persisted in the database.
My DB schema:
I have 4 table: Model, ModelCategory, ModelCategoryKey, ModelParameter.
ModelCategory 1--n Model 1--m ModelParameter
ModelCategory 1--n ModelCategoryKey
ModelCategoryKey 1--n ModelParameter
After the user choose a ModelCategory from the form's (form based on Model entity) dropdown i have to update the form with ModelParamater rows, but it's number and default values depends on ModelCategory 1--n ModelCategoryKey assocaiton.
I've tried to attach NEW ModelParameter entities to the main Model entity during the PRE_BIND event (also set their default values) and it seems working fine, but when i add the 'parameters' with a 'collection' typed element to the form i get the next error:
Entities passed to the choice field must be managed. Maybe persist them in the entity manager?
Clearly my entities can't be (and shouldn't be) persisted at this time.
All ideas are welcome!
UPDATE:
Modifying the form after preSubmit/preBind:
$form->add('parameters','collection',array(
'type' => new ModelParameterType(),
));
OR
$form->add(
$this->factory->createNamed('parameters','collection',null,
array(
'type' => new ModelParameterType()
))
);
where the 'factory' attribute is a FormFactoryInterface. The error message is the same.
UPDATE2:
Further investigation proved, that if i don't add "default" entities to the assocation. Then it works without error.
Here is the source of my form modifying method:
public function preSubmit(FormEvent $event) {
$form = $event->getForm();
$id = $event->getData()['modelCategory'];
$entity = $form->getData();
$categoryKeys = $this->em->getRepository('MyBundle:ModelCategoryKey')->findByModelCategory(
$this->em->getReference('MyBundle:modelCategory',$id)
);
foreach ($categoryKeys as $key) {
$param = new ModelParameter();
$param->setModel($entity);
$param->setKey($key);
$entity->addParameter($param);
}
$form->add(
$this->factory->createNamed('parameters','collection',null,
array(
'type' => new ModelParameterType(),
'allow_add' => true,
'cascade_validation' => true
))
);
}
SEEMS TO BE SOLVED BY
I have just commented out the $param->setModel($entity); line and it seems to be working fine. I will work this out more and will share the experience, if it realy works.

choice field accepts only managed entities, as the value is set to the entity after submit, and form posts only entities ID, so it must be saved beforehand.
You don't need choice field - you need collection of parameter subforms.
$formBuilder
->add('category', 'category_select')
->add('parameters', 'collection', array('type' => 'parameter'))
;
I'm assuming here that category_select is choice field with categories and parameter is subform with it's own values, depending on your parameter structure.
When you have category in your controller, you can bind the newly created entity with added Parameter entities with their key set, depending on ModelCategoryKey.

I've managed to solve my problem, so here it is what i've found out:
It is enough to add the newly created object by the adder function of the inverse side. I don't have to call the owning side's setter.
Inverse side adder function must be modified, that it calls the owning side's setter.
Inverse side adder function must check if the object is not in the collection already.
PRE_SET_DATA event happens, when the form is created. (so in new entities it is empty, and in old ones it is filled)

Related

Symfony 4, Object and SubObjects, missing foreign keys

I am not able to find the trick to get the following.
Say I have two Entity: Main and Minor, Main one-to-many Minor, mainId being the foreign key field.
I wish to have both a (Minor) form to create a Minor object, such that users may select its Main object from a list of already available Main objects, and a (Main) form to create a Main object and possibly many different Minor (sub)objects at once.
The issue is that in the latter case, I am not able to save the foreign key.
For the Minor form, I define:
$builder ->add('minorTitle')
->add('Main', EntityType::class, array(
'class' => Main::class,
'choice_label' => 'mainTtile',
'label' => 'main'))
have 'data_class' => Minor::class, and it works fine.
For the Main form, I tried:
$builder
->add('mainTitle')
->add('Minors', CollectionType::class, array(
'entry_type' => MinorType::class,
'allow_add' => true,
'label' => 'Minor'
))
'data_class' => Main::class`
So the Minor form is indeed embedded as a subform within the Main one. To add more subforms, I have some JS as suggested in CollectionType. To avoid to display the Main field in the Minor subforms, I have hacked a little the prototype, by something like:
newWidget = newWidget.replace(newWidget.match(/\B<div class="form-group"><label class="required" for="main_Minors___name_Main">Main<\/label><select id="main_Minors___name_Main" name="main\[Minors\]\[__name__\]\[Main\]" class="form-control">.*<\/select>\B/g),"");
A user is able to create a Main object, and many Minor ones too, but the id of the former is not saved as the foreign keys of the latter ones. I have tried to fix things within the Main Controller by something like (or variants):
public function new(Request $request): Response {
$em = $this->getDoctrine()->getManager();
$main = new Main();
$form = $this->createForm(MainType::class, $main);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$postData = $request->request->get('main');
$minors = array();
foreach($postData['Minors'] as $key => $obj){
$minors[$key]= new Minor();
$minors[$key]->setMain($main);
$minors[$key]->setMinorTitle($obj['minorTitle']);
$em->persist($minors[$key]);
}
$em->persist($main);
$em->flush();
}
but either it does not work, or it saves twice the same subobject (only once with the correct foreign key).
(Maybe, I could fix by two different MinorType classes, but I would like to avoid that)
Thanks
Just a number of hints.
your form types should have the data_class option set to the appropriate class.
your form field names should match the property name on the entity. (and by default, all property names are in camelCase, lowercase first char ... in symfony)
after 1. and 2. you get proper entities by just calling $form->getData() (or, as you might have noticed, when you give the createForm call an entity, it will be modified by the form component - this might not always be intended. consider Data Transfer Objects (DTO) for when it's not intended.)
your CollectionType field should have option byReference set to false, such that the setters get used on the collection field (Main::setMinors, in this case).
usually the one-to-many side (i.e. Main class) can get away with:
public function setMinors(array $minors) {
foreach($minors as $minor) {
$minor->setMain($this); // set the main, just to be safe
}
$this->minors = $minors; // set the property Main.minors
}
but you should not do this in setMain in reverse too (it's also not so trivial. alternative to setMinors are addMinor and removeMinor, there are benefits and costs for either solution, but when it comes to forms, they are quite equivalent, I would say)
on Main if you set the cascade={"PERSIST"} option on the OneToMany (i.e. #ORM\OneToMany(targetEntity="App\Entity\Minor", cascade={"PERSIST"})), you don't have to explicitly call persist on all minors, they will get persisted as soon as you persist (and flush) the Main object/instance.
Finally, either add an option to your minor type, to omit the main form field, or add a new form type MainMinorType (or whatever) that doesn't have the main form field (extend MinorType and remove the main field). This removes the necessity for dirty hacks ;o)
However, overall, if you don't set the minors on the main in a bi-directional relationship, the results are not clearly defined. (just assume for a moment, A has a link to B, but B doesn't have a link to A, but should have, because it's a bi-directional relationship. It could mean, that the link has to be established. It could also mean, that the link should be removed. So, to be safe and clearly communicate what is intended, set both sides!) And ultimately, this might be the reason it doesn't work as intended.
update
To elaborate on point 7. Your MinorType could be amended like this:
class MinorType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
// ... other fields before
if(empty($options['remove_main_field'])) {
// field is the same, but isn't added always, due to 'if'
$builder->add('main', EntityType::class, [
'class' => Main::class,
'choice_label' => 'mainTtile',
'label' => 'main'
]);
}
// ... rest of form
}
public function configureOptions(OptionsResolver $resolver) {
// maybe parent call ...
$resolver->setDefaults([
// your other defaults
'remove_main_field' => false, // add new option, self-explanatory
]);
}
}
in your MainType you had, the following, to which I added the new option
->add('Minor', EntityType::class, array(
'class' => Minor::class,
'remove_main_field' => true, // <-- this is new
))
now, this will remove the main field from your minors forms, when it's embedded in your main form. the default is however, to not remove the main field, so when you edit a minor by itself, the main field will be rendered, as it was before ... unless I made a mistake in my code ;o)

Symfony2: manually add a NotBlank constraint

I am dynamically building forms. This works great, but now I need to check that same fields are not empty. The related fields are get from the DB, so I cannot use annotations.
I tried the following:
$constraints = array();
$constraints[] = new NotBlank(array('message' => 'Please enter something'));
$params['constraints'] = $constraints;
$formBuilder->add($dynamic_name, $dynamic_type, $params);
This works only for the main form (not subforms - whereas I put the 'cascade_validation' => true at each form / subform creation), and the most annoying is that the error message isn't displayed (whereas custom errors that I manage in an external validator are displayed fine).
Any idea?

ZF2 refresh input filter after dynamicly adding elements to form

I have a form that triggers on event in __construct method to load some items from another modules . So far so good , a field set is loaded from the other module and added to the form and in the request->getPost() I have the data for the elements inside the fieldset , but the $form->getData() doesn't have the data for the fieldset.
I am calling $form->getInputFilter() before adding this fieldsets to the form and it seems that calling the $form->getInputFilter() dosn't creates the filters for the newly added elements . so how can i create inputfilters for the dynamic events without recreating the hole filters again ?
Or should i just delay calling $form->getInputFilter() untill all of the elemnts have been added to the form ?
I also added some elements to the form later what was ignored by the input filter.
My solution is most likely not exactly the best one, but as you haven't received any other answers yet, here's what I did:
I added
use Zend\InputFilter\Factory as InputFactory;
in the class where I'm validating the form data and then used
$factory = new InputFactory();
$form->getInputFilter()->add($factory->createInput(array(
'name' => 'title_str',
'required' => true,
'filters' => array(
array('name' => 'Int'),
),
)));
#Afterdark017 that works and also i think it is possible to reset the filters.
protected function resetFilters(){
$this->filter = null;
$this->hasAddedInputFilterDefaults = false;
}
but i have not tested this yet.

Populate values to checkbox while editing in zend framework

I am trying to populate the values into check box. I want check box to be checked when there is value stored in database.
This is my code in form:
$form ['test_1'] = new Zend_Form_Element_Checkbox('test_1');
$form['test_1']->setLabel('test1')->setCheckedValue('1');
$form ['test_2'] = new Zend_Form_Element_Checkbox('test_2');
$form['test_2']->setLabel('test2')->setCheckedValue('2');
If there is value 1 in database i want first check box to be checked and if its 2 then 2nd checkbox needs to be checked.
What do i need to do in the controller.
Could anyone please help me on this issue.
The easiest way would be to fetch the values from the database as an array that maps to the form input elements, e.g. return a row like
array('test_1' => 'value of checkbox', 'test_2' => 'value of checkbox');
You could then simply call $form->populate($values) and let do Zend_Form do the setting, e.g. in your controller do
public function showFormAction()
{
$form = $this->getHelper('forms')->get('MyForm');
$data = $this->getHelper('dbGateway')->get('SomeTable');
$form->populate($data->getFormData());
$this->view->form = $form;
}
Note: the helpers above do not exist. They are just to illustrate how you could approach this. Keep in mind that you want thin controllers and fat models, so you should not create the form inside the controller, nor put any queries in there.

Drupal select list only submitting first character

I have a select list I've created in a form alter, however, when I select an option and submit the value, only the first digit gets stored in the database. I know this has something to do with how the array is formatted, but I can't seem to get it to submit properly.
function addSR_form_service_request_node_form_alter(&$form, $form_state) {
$form['field_sr_account'] = array( '#weight' => '-50',
'#type' => 'select',
'#title' => 'Select which account',
'#options' => addSR_getMultiple());
//Custom submit handler
$form['#submit'][] = 'addSR_submit_function';
}
function addSR_submit_function{
$form_state['values']['field_sr_account'] = array('0' => array('value' => $form['#field_sr_account']));
Below is the function that returns the associative array. It is returning the proper options, as I can view the correct value/option in the HTML source when the page loads
//The values returned are not the problem, however, the format of the array could be..
function addSR_getMultiple(){
$return = array();
$return['one'] = 'Choice1'
$return['two'] = 'Choice2'
return $return;
}
Update:
Drupal 6: Only inserting first character of value to MySQL
I had a similar issue with the same field. However, in that case, I knew the value I wanted to submit, and I was able to assign the value to the field in the form alter, before the form was submitted. The difference with this issue, is that I don't know the value of the field until it is submitted, so I can't "assign" it in the form alter. How can I assign it the same way in the submit handler.
Edit after question update (and discovery of root problem within the linked separate question):
As you are trying to manipulate CCK fields, and those have pretty special handling mechanisms compared to 'standard' Drupal FAPI form elements, you should probably read up on CCK form handling in general, and hook_form_alter() and CCK fields and CCK hooks in particular. Glancing at those documentations (and the other CCK articles linked in the left sidebar), it looks like there should be a straight forward solution to your problem, but it might require some digging.
As a potential 'quick fix', you could try keeping your current approach, and adjust the submitted value on validation, somewhat like so:
function addSR_form_service_request_node_form_alter(&$form, $form_state) {
$form['field_sr_account'] = array(
'#weight' => '-50',
'#type' => 'select',
'#title' => 'Select which account',
'#options' => addSR_getMultiple()
);
// Add custom validation handler
$form['#validate'][] = 'addSR_validate_function';
}
function addSR_validate_function (&$form, &$form_state) {
// Assemble result array as expected by CCK submit handler
$result = array();
$result[0] = array();
$result[0]['value'] = $form_state['values']['field_sr_account'];
// Set this value in the form results
form_set_value($form['field_sr_account'], $result, $form_state);
}
NOTE: This is untested code, and I have no idea if it will work, given that CCK will do some stuff within the validation phase as well. The clean way would surely be to understand the CCK form processing workflow first, and manipulating it accordingly afterward.