Using a Pagerfanta/ non ArrayAccess list in a bulk form - forms

I'm adding checkboxes for bulk actions to a CRUD list, using the solution provided here.
However, my results are paged with Pagerfanta, so it seems I need to use a DataMapper in my form.
I have tried various solutions, but cannot get the selected fields to be available in my form data:
class ModelEntitySelectionType extends AbstractType implements DataMapperInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('model_id', EntityType::class, [
'required' => false,
'class' => ModelFile::class,
'choice_label' => 'id',
'property_path' => '[id]', # in square brackets!
'multiple' => true,
'expanded' => true
])
->add('action', ChoiceType::class, [
'choices' => [
'Delete' => 'delete'
]
])
->add('submit', SubmitType::class, [
'label' => 'Process'
])
->setDataMapper($this)
;
}
public function setDefaultOptions(ExceptionInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => null,
'csrf_protection' => false
));
}
public function mapDataToForms($data, $forms)
{
// there is no data yet, so nothing to prepopulate
if (null === $data) {
return;
}
$formData = [];
/** #var FormInterface[] $forms */
$forms = iterator_to_array($forms);
$forms['model_id']->setData($formData);
}
public function mapFormsToData($forms, &$data)
{
//$forms = iterator_to_array($forms);
$data = [
'model_id' => iterator_to_array($data)
];
}
The missing piece is when I investigate mapFormsToData with a debugger:
$forms is a RecursiveIteratorIterator
$data is a PagerFanta object
I understand how I have to "loop" through the PagerFanta object, because it doesn't have ArrayAccess, but where is the data of which checkboxes have actually been ticked? Also, my other form fields (action) are not accessible here

I think your approach is problematic. The Form component is meant to modify the object passed to it, which is - as far as I can tell - not what you want. You don't want to modify a Pagerfanta object, you want to select entities for bulk actions.
So to solve your problem, the very very raw things that have to happen: A <form> must be displayed on the page with a checkbox for every entry that's a candidate for the bulk action, with some button(s) to trigger the bulk action(s).
Your form - besides the entry for checkboxes - is alright I guess and not really your problem, as far as I can tell. You're not even interested in editing the Pagerfanta object (I hope) and just want the selection. To do that, we provide the collection of objects that are queued to be displayed on the page to the form via an option, and then use that option to build the field (read: pass the collection to the EntityType field).
Adding the collection to the form (call) as an option:
Somewhere in your controller, you should have something like:
$form = $this->createForm(ModelEntitySelectionType::class, $pagerfanta);
Change this to:
$form = $this->createForm(ModelEntitySelectionType::class, [], [
'model_choices' => $pagerfanta->getCurrentPageResults(),
]);
the method getCurrentPageResults return the collection of entities for the current page (obviously). The empty array [] as the second parameter is ultimately the object/array you're trying to edit/create. I've chosen an array here, but you can also make it a new action class (like a DTO) e.g. ModelBulkAction with properties: model and action:
class ModelBulkAction {
public $model;
public $action;
}
Note these kinds of objects only make sense if used in more than one place - then the call would be:
$form = $this->createForm(ModelEntitySelectionType::class, new ModelBulkAction(), [
'model_choices' => $pagerfanta->getCurrentPageResults(),
]);
Pass the choices to the sub form:
The Form component will complain, if you provide an option to a form, which doesn't expect that option. That's the purpose of AbstractType::configureOptions(OptionsResolver $resolver). (side note: I don't know, what your setDefaultOptions is supposed to achieve, tbh, with an ExceptionInterface nonetheless. No clue, really).
public function configureOptions(OptionsResolver $resolver) {
$resolver->setRequired([
'model_choices', // adds model_choices as a REQUIRED option!
]);
$resolver->setDefaults([
// change null to ModelBulkAction::class, if applicable
'data_class' => null,
]);
}
and finally actually passing the collection to the entity type sub form:
// in ModelEntitySelectionType::buildForm($builder, $options)
$builder->add('model', EntityType::class, [
'required' => false,
'class' => ModelFile::class,
'choice_label' => 'id',
'choices' => $options['model_choices'], // set the choices explicitly
'multiple' => true,
'expanded' => true,
])
// ...
;
Also, your data mapping is not needed any more and should be removed.
Adding the form widgets to the output
This is pretty much similar to the Stack Overflow question and answer you linked. However, the keys in the form are different, because my approach is slightly different:
{{ form_start(form) }}
{% for entity in pagerfanta %}
{# stuff before the checkbox #}
{{ form_widget(form.model[entity.id]) }}
{# stuff after the checkbox #}
{% endfor %}
{# place the selection of action somewhere! and possibly the "submit" button #}
{{ form_widget(form.action) }}
{{ form_end(form) }}
(note: this will probably show the id of the entry next to the checkbox, since that's your choice_label, I believe this can be removed by: {{ form_widget(form.model[index], {label:false}) }} or alternatively by setting the choice_label to false instead of 'id').
Getting your bulk entities
After $form->handleRequest($request); you can check for submission and the form values:
if($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
// $data['model'] contains an array of entities, that were selected
// $data['action'] contains the selection of the action field
// do the bulk action ...
}
If you implemented the ModelBulkAction approach, $data is an object of that kind and has $data->model as well as $data->action for you to use (or pass on to a repository).
More stuff
Obviously the model_choices option can be named almost any way you like (but should not clash with existing options the AbstractType may have).
To make an entity selectable (besides the checkbox), you can for example use <label for="{{ form.model[index].vars.id }}"><!-- something to click --></label> as a non-javascript approach (may add styling). With js it's pretty much irrelevant because you probably just need to select the first checkbox in the row.
Alternatives
Alternative to providing the collection of objects to the form, you could theoretically also provide a list of ids and use the ChoiceType instead of the EntityType. There is nothing to be gained from this though.

Related

Form field order in Symfony 4 formevent listener

I've been struggling to get this done. In the docs, there doesn't seem to have any option to order a form field.
So, I have firstname and lastname fields in my form like this:
->add('firstname', TextType::class, [
'required' => true
])
->add('address', TextareaType::class, [
'required' => true
])
Then, I added an event listener like so:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$plan = $event->getData();
$form = $event->getForm();
if (!$plan->getReferenceId()) {
$form->add('ref', TextType::class, [
'label' => 'Reference Number',
'required' => true,
// position should be after the firstname and before address
]);
}
}
In some of the threads, there was a 'position' attribute that you can add but that was for Symfony 2 and that doesn't exist now in S4.
I guess this can be done in twig form_widget but I am using twig templates which is used in other forms as well. So I am hoping to get this done in the formtype if possible.
it seems to be a really required issue on Symfony github
here: https://github.com/symfony/symfony/issues/5827
and here: https://github.com/symfony/symfony/pull/11241#issuecomment-288462731 where Fabien personaly declined the possible solution on PR
i have the same problem and it seems that the only solution is order the fields manually printing them with form_row or form_widget one by one on the templates.
like this: https://symfony.com/doc/current/form/form_customization.html

Symfony4 Forms - How do you conditionally disable a form field?

So what is the best way to have a form render effectively the same form over and over again, with conditionally disabled fields based on the Entity's property values?
I have an Invoice Entity and need a form for creating the invoice, and also the same form with various fields disabled at various stages of the invoicing process (generated, sent, paid etc).
I think the simplest answer is to disable them dynamically in the twig template via form_row options but surely this will affect server side validation of the form as it is not aware the field has been disabled?
What is the best way to disbale a field based on a value in the database?
EDIT 1:
Changed question from Dynamically disable a field in the twig template or seperate class for each form? to Symfony4 Forms - How do you conditionally disable a form field?
Thanks to #Cerad. The answer is in fact Form Events
In the form type (App\Form\InvoicesType for me), add a method call to the end of the builder:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$plus_thirty_days = new \DateTime('+28 days');
$builder
->add('client', EntityType::class, array(
'class' => Clients::class,
'choice_label' => 'name',
'disabled' => false,
) )
// the event that will handle the conditional field
->addEventListener(
FormEvents::PRE_SET_DATA,
array($this, 'onPreSetData')
);;
}
and then in the same class, create a public method named the same as the string in the array (onPreSetData for this example):
public function onPreSetData(FormEvent $event)
{
// get the form
$form = $event->getForm();
// get the data if 'reviewing' the information
/**
* #var Invoices
*/
$data = $event->getData();
// disable field if it has been populated with a client already
if ( $data->getClient() instanceof Clients )
$form->add('client', EntityType::class, array(
'class' => Clients::class,
'choice_label' => 'name',
'disabled' => true,
) );
}
From here you can update the field to be any valid FormType and specify any valid options as you would a normal form element in the From Builder and it will replace the previous one, laving it in the same original position in the form.

Add a choice list(dropdown) from 2 different entities in a single table in Symfony 2

I am looking for a method on how to possibly create a single dropdown for my form in symfony 2 that contains the value of the field 'abbr1' and 'abbr2' from a single record in table Params.
Lets say i have a single record in my table Params.
id: 1
title: sample
abbr1: qw12
abbr2: er34
Now i want to pick abbr1 and abbr2 as the value of a single dropdown. I have created a form but i dont know how to make both of them a choice. I can only pick them as a property one at a time. Here is my code:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'desiredAbbr',
'entity',
array(
'class' => 'FooBarBundle:Params',
'property' => 'abbr1',
//'property' => 'abbr2',
)
)
->add('save','submit',array('label'=>'Submit'))
;
}
Any Suggestions are much appreciated. Thanks a lot.
Update:
The expected dropdown value would look like this in html format:
{% for par in parameters %}
<select>
<option>{{param.abbr1}}</option> {# qw12 #}
<option>{{param.abbr2}}</option> {# er34 #}
</select>
{% endfor %}
ok, I missed that you want them as value, not as label. Then you should change you form like this
$choices = $options['abbrChoices'];
$builder->add('desiredAbbr', ChoiceType::class, array(
'choices' => $choices,
));
// in configureOptions method
$resolver->setDefaults(array(
'abbrChoices' => array(),
));
In controller where you create the form
$params = $this->getDoctrine()->getRepository('AppBundle:Params')->findAll();
$choices = array();
foreach ($params as $p) {
// key will be used as option value, value as option title
$choices[$p->getAbbr1()] = $p->getAbbr1();
$choices[$p->getAbbr2()] = $p->getAbbr2();
}
$form = $this->createForm(myform::class, array(), array('abbrChoices' => $choices));
BUT. How are you going to use this choice?

Symfony2 form unchecked checkbox not taken into account, why?

When I send a form with an unchecked checkbox, if the related entity property equals true, then it does not change to false.
The other way round (setting property to true when the form is sent with a checked checkbox) works fine, as well as all the forms other fields saving.
Here is how I build the form and declare the related property:
// --- Form creation function EntityType::buildForm() ---
$builder->add('secret', 'checkbox', array( 'required' => false ));
// --- Entity related property, Entity.php file ---
/** #ORM\Column(name="secret", type="boolean") */
protected $secret;
EDIT: The issue happens because the form is submitted using a PATCH request.
In Symfony, the Form::submit method is called by a Request Handler with this line:
$form->submit($data, 'PATCH' !== $method);
As a result the Form::submit $clearMissing parameter is set to false in the case of a PATCH request, thus leaving the non-sent fields to their old value.
But I do not know how to solve the problem. If I explicitely pass a JSON {secret: false} to the Symfony framework when the checkbox is not checked, it will interpret it as the "false" string and consider that a true value, thus considering the checkbox checked...
NB. I have exactly the same issue with an array of checkboxes using a choice field type (with multiple and extended to true) linked to a Doctrine Simple Array property: as soon as a given checkbox has been sent once as checked, it is impossible to set back the related property to false with subsequent unchecked submissions.
Non of above-mentioned didn't help me.
So, I am using this...
Explanation
Resolution for this issue when "PATCH" method was used, was to add additional hidden "timestamp" field inside of a form type and to have it next to the checkbox of issue in twig file. This is needed to pass something along with the checkbox, that would definitely change - time-stamp will change.
Next thing was to use PRE_SUBMIT event and to wait for form field to arrive and if it not set, I would set it manually... Works fine, and I don't mind extra code...
FormType
$builder
...
->add('some_checkbox')
->add('time_stamp', 'hidden', ['mapped' => false, 'data' => time()])
...
Twig
{{ form_widget(form.time_stamp) }}
{{ form_widget(form.some_checkbox) }}
PRE_SUBMIT event in builder
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use($options) {
$data = $event->getData();
$form = $event->getForm();
if (!$data) {
return;
}
/* Note that PATCH method is added as an option for "createForm"
* method, inside of your controller
*/
if ($options["method"]=="PATCH" && !isset($data['some_checkbox'])) {
$form->getData()->setSomeCheckbox(false);//adding missing checkbox, since it didn't arrive through submit.. grrr
}
});
The issue happens because the form is submitted using a PATCH request.
This has lead to open this Symfony issue.
As explained, one workaround is to explicitely send a specific reserved value (for instance the string '__false') when the checkbox is unchecked (instead of sending nothing), and replace this value by 'null' using a custom data transformer in the form type:
// MyEntityFormType.php -- buildForm method
$builder->add('mycheckbox', ...);
$builder->get('mycheckbox')
->addViewTransformer(new CallbackTransformer(
function ($normalizedFormat) {
return $normalizedFormat;
},
function ($submittedFormat) {
return ( $submittedFormat === '__false' ) ? null : $submittedFormat;
}
));
The case with the 'choice' field can't be solved the same way. It is actually a bug of Symfony, dealt with in this issue.
What version of Symfony are you using?
There should exist some code dedicated to the situation you're writing about, in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php, in Form::submit():
// Treat false as NULL to support binding false to checkboxes.
// Don't convert NULL to a string here in order to determine later
// whether an empty value has been submitted or whether no value has
// been submitted at all. This is important for processing checkboxes
// and radio buttons with empty values.
if (false === $submittedData) {
$submittedData = null;
} elseif (is_scalar($submittedData)) {
$submittedData = (string) $submittedData;
}
Located at lines 525-534 for me. Could you check this works properly for you?
Another lead would be a custom form subscriber that do not work exactly as intended - by overwriting the provided value.
It's probably because the field isn't required on you schema. you can provide a default value to the checkbox with the following:
$builder->add('secret', 'checkbox', array(
'required' => false,
'empty_data' => false
));
See here or here
This solution works for me.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('isActive', CheckboxType::class, array(
'required' => false
))
$builder->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $e {
$entity = $e->getData();
$form = $e->getForm();
$isActive = empty($_POST[$form->getName()]['isActive']) ? false : true;
$entity->setIsActive($isActive);
});
}
Another possibility is to add a hidden element and make this with Javascript. It will fail in 0.1 % of the people that use a browser without javascript.
This is a simple example for a multiple checkboxes FormType element:
->add('ranges', ChoiceType::class, array(
'label' => 'Range',
'multiple' => true,
'expanded' => true,
'choices' => array(
null => 'None',
'B1' => 'My range',
)
))
<script>
$(document).ready(function() {
function updateDynRanges(object) {
if (object.prop("checked")) {
document.getElementById('ranges_0').checked = 0;
} else {
document.getElementById('ranges_0').checked = 1;
}
}
// On page onload
$('#ranges_1').change(function() {
updateDynRanges($(this));
});
updateDynRanges($('ranges_1'));
}
</script>
If after testing works you can just add a visibility:false to the second checkbox.
Twig template:
{{ form_label(form.dynamicRanges) }}<br>
{{ form_widget(form.dynamicRanges[1]) }}
<div class="hidden">{{ form_widget(form.ranges[0]) }}</div>
Looks like an ugly workaround, but I just wanted to compete with the other ugly suggested workarounds, in this case mostly updating the twig template.

Symfony 2.3 pass custom data to entity form, using choice or other type

SETUP:
Main entity with a related entity with ManyToOne relation.
Main entity has a formType with the related entity added.
The related entity is a big object with a lot of fields and related objects, and very slow to get.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('relatedEntity', 'entity', array(
'class' => 'ProjectName\RelatedEntityBundle\Entity\RelatedEntity',
'query_builder' => function (EntityRepository $er) {
$queryBuilder = $er->createQueryBuilder('relatedEntity');
$queryBuilder->resetDQLPart('select');
$queryBuilder->resetDQLPart('from');
$queryBuilder->select('relatedEntity')
->distinct(true)
->from('ProjectNameRelatedEntityBundle:RelatedEntity', 'relatedEntity');
return $queryBuilder;
},
....
....
}
Template:
(relateEntity has a __toString() function defined to show its name).
{{ form_label(form.relatedEntity) }}
{{ form_widget(form.relatedEntity) }}
{{ form_errors(form.relatedEntity) }}
QUESTIONS:
The Main entity as shown above, will get all objects and pass them
to the template. It works perfectly but it is very slow since the
related entity objects are big and the query may take more 10
seconds to finish hydrating all the object data.
How could I select only some fields from my related entity and show them in the template without getting all objects hydrated?
Is it possible to use the choice option or another type instead of
the default entity type to get only some fields of the related
entity and show them in the template?
How could I build a custom query hydrated as a simple array of key value, and pass that array to the formType, to the queryBuilder of my related entity field?
Finally, in case its not possible to get only some fields to be
shown in the template, should I avoid symfony 2 forms and make a
custom management of the related entity?
TESTS:
I cant seem to build the form with the choice type by passing just an array to show a selectBox with the id and name of my related entity in the template. I always get the same error, asking me to insert an array of entity objects in that choiceS option.
Lets look at some examples at the formType, buildForm function of the main entity:
WORKS, default Symfony 2 generated code with null type:
->add('relatedEntity', null, array('label'=> 'relatedEntity'))
WORKS, with 'entity' type and a simple queryBuilder:
->add('relatedEntity', 'entity', array(
'class' => 'ProjectName\RelatedEntityBundle\Entity\RelatedEntity',
'query_builder' => function (EntityRepository $er) {
$queryBuilder = $er->createQueryBuilder('relatedEntity');
$queryBuilder->resetDQLPart('select');
$queryBuilder->resetDQLPart('from');
$queryBuilder->select('relatedEntity')
->from('ProjectNameRelatedEntityBundle:RelatedEntity', 'relatedEntity');
return $queryBuilder;
},
'property' => 'descripcion'
))
DOESNT WORK with 'choice' type, with 'choices' option passing an array of values:
$arrayValues = array('1'=>'name1', '2'=>'name2', '3'=>'name3');
->add('relatedEntity', 'choice', array(
'choices' => $arrayValues,
'multiple' => false,
'label'=> 'relatedEntity'
))
DOESNT WORK with 'entity' type, with 'choices' option passing an array of values:
$arrayValues = array('1'=>'name1', '2'=>'name2', '3'=>'name3');
->add('relatedEntity', 'entity', array(
'class' => 'ProjectName\RelatedEntityBundle\Entity\RelatedEntity',
'choices' => $arrayValues ,
'multiple' => false,
'label'=> 'relatedEntity'
))
I have also tested trying to hack the choices input requeriment by building an array of objets of my related entity, but it asks me to persists those entities before being sent to the choice type.
The problem is your form element which requires its content to be an entity, which is an instance of class ProjectName\RelatedEntityBundle\Entity\RelatedEntity, but you pass an array as choices:
$arrayValues = array(
'1'=>'name1',
'2'=>'name2',
'3'=>'name3'
);
On the other hand, when you use a choice-element and add the array, your form element will return a string, whereas your entity requires relatedEntity to be an instance of the above mentioned class.
Either way, you have to ensure the data you add or retrieve from the element matches your requirements.
What you can do, is make it a choice-element and remove the class-restriction (as you have tried). Then, to ensure it will return an entity-instance rather than a string you can use Form Events. You could use FormEvents::SUBMIT or FormEvents::PRE_SUBMIT to check which entity name was selected and perform a query to fetch the corresponding entity, e.g. something like:
$objectRepository->findEntityBy(array('name' => $name));