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:
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).
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(),
]);
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.
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'
).
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).
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.
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.