问题
I need a form field to choose from thousands of entities, so a dynamic choice system like select2 (with AJAX) is perfectly suited.
My AJAX endpoint works fine, but the custom form type does not work:
class Select2AjaxDataCategoryType extends AbstractType
{
/**
* @var EntityManagerInterface
*/
private $entityManager;
/**
* @var RouterInterface
*/
private $router;
public function __construct(EntityManagerInterface $entityManager,
RouterInterface $router)
{
$this->entityManager = $entityManager;
$this->router = $router;
}
public function getParent()
{
return ChoiceType::class;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->resetModelTransformers();
$builder->resetViewTransformers();
$builder->addModelTransformer(new CallbackTransformer(
function (?DataCategory $dc) {
dump('model transform is called ' . ($dc ? $dc->getId()->toString() : 'null'));
return $dc ? $dc->getId()->toString() : '';
},
function ($id) : ?DataCategory{
dump('model reversetransform is called ' . $id);
$dc = $this->entityManager->getRepository(DataCategory::class)->find($id);
if($dc === null)
throw new TransformationFailedException("Konnte keine Datenkategorie mit ID $id finden");
return $dc;
}
));
$builder->addViewTransformer(new CallbackTransformer( // Identity !!!
function ($dc) {
dump('view transform is called ' . $dc);
return $dc;
},
function ( $id) {
dump('view reversetransform is called ' . $id);
return $id;
}
));
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { // makes validation pass
$data = $event->getData();
dump($data); // select2'd id, correct
dump($event->getForm()->getName()); // name of my form field
$event->getForm()->getParent()->add( // so this is lik "overwriting"? Documented nowhere :-/
$event->getForm()->getName(),
ChoiceType::class,
['choices' => [$data => $data]]);
$event->getForm()->getParent()->get($event->getForm()->getName())->setData($data);
});
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired('currentDataCategory');
$resolver->setAllowedTypes('currentDataCategory', [DataCategory::class]);
$resolver->setDefaults([
'attr' => [
'data-ajax' => '1',
'data-ajax-endpoint' => $this->router->generate('data-category-manage-select2')
]
]);
}
}
When using this form type, it seems to work, but finally no entity object is returned, but null
. According to symfony debug toolbar however, the value is received:
Also the dumps indicate that the view and model transformers were called:
For the sake of completeness (I hope we'll find a perfect solution and help others), here is my js code (it works):
$('select[data-ajax=1]').select2({
theme: "bootstrap4",
placeholder: "Bitte wählen",
ajax: {
url: function() { return $(this).data('ajax-endpoint');},
dataType: 'json',
data: function (params) {
var query = {
search: params.term,
page: params.page || 0
}
// Query parameters will be ?search=[term]&page=[page]
return query;
}
}
});
回答1:
I have solved the problem, here is my complete solution:
$('select[data-ajax=1]').select2({
theme: "bootstrap4",
placeholder: "Bitte wählen",
ajax: {
url: function() { return $(this).data('ajax-endpoint');},
dataType: 'json',
data: function (params) {
var query = {
search: params.term,
page: params.page || 0
}
// Query parameters will be ?search=[term]&page=[page]
return query;
}
}
});
The new form type is fixed for one class DataCategory
, and works both for single and multiple select's.
I have build-in a distinction between select2 frontend and the standard EntityType
(mainly for testing reasons, because the new select2 based approach does not allow PHPUnit tests that use symfony's Client
(WebTestCase)): If there are less than 50 DataCategory entities in the DB, the field falls back to EntityType
class Select2AjaxDataCategoryType extends AbstractType
{
/**
* @var EntityManagerInterface
*/
private $entityManager;
/**
* @var RouterInterface
*/
private $router;
private $transformCallback;
public function __construct(EntityManagerInterface $entityManager,
RouterInterface $router)
{
$this->entityManager = $entityManager;
$this->router = $router;
$this->transformCallback = function ($stringOrDc) {
if (is_string($stringOrDc)) return $stringOrDc;
else return $stringOrDc->getId()->toString();
};
}
public function getParent()
{
if($this->entityManager->getRepository(DataCategory::class)->count([]) > 50)
return ChoiceType::class;
else
return EntityType::class;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if($this->entityManager->getRepository(DataCategory::class)->count([]) > 50) {
$builder->addModelTransformer(new CallbackTransformer(
function ($dc) {
/** @var $dc DataCategory|DataCategory[]|string|string[] */
/** @return string|string[] */
dump('model transform', $dc);
if($dc === null) return '';
if(is_array($dc)) {
return array_map($this->transformCallback, $dc);
} else if($dc instanceof Collection) {
return $dc->map($this->transformCallback);
} else {
return ($this->transformCallback)($dc);
}
},
function ($id) {
dump('model reversetransform', $id);
if (is_string($id)) {
$dc = $this->entityManager->getRepository(DataCategory::class)->find($id);
if ($dc === null)
throw new TransformationFailedException("Konnte keine Datenkategorie mit ID $id finden");
dump($dc);
return $dc;
} else {
$ret = [];
foreach($id as $i){
$dc = $this->entityManager->getRepository(DataCategory::class)->find($i);
if ($dc === null)
throw new TransformationFailedException("Konnte keine Datenkategorie mit ID $id finden");
$ret[] = $dc;
}
return $ret;
}
}
));
$builder->resetViewTransformers();
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$dataId = $event->getData();
dump('presubmit', $dataId, $event->getForm()->getConfig()->getOptions()['choices']);
if(empty($dataId))
return;
$name = $event->getForm()->getName();
if (is_array($dataId)) { // multiple-true-case
if (!empty(array_diff($dataId, $event->getForm()->getConfig()->getOptions()['choices']))) {
$options = $event->getForm()->getParent()->get($name)->getConfig()->getOptions();
$options['choices'] = array_combine($dataId, $dataId);
$event->getForm()->getParent()->add($name, Select2AjaxDataCategoryType::class, $options);
$event->getForm()->getParent()->get($name)->submit($dataId);
$event->stopPropagation();
}
} else { // multiple-false-case
if($dataId instanceof DataCategory){
$dataId = $dataId->getId()->toString();
throw new \Exception('Hätte ich nicht erwartet, sollte string sein');
}
if (!in_array($dataId, $event->getForm()->getConfig()->getOptions()['choices'])) {
$options = $event->getForm()->getParent()->get($name)->getConfig()->getOptions();
$options['choices'] = [$dataId => $dataId];
$event->getForm()->getParent()->add($name, Select2AjaxDataCategoryType::class, $options);
$event->getForm()->getParent()->get($name)->submit($dataId);
$event->stopPropagation();
}
}
});
// $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){
// dump("pre set data", $event->getData());
// });
} else {
}
}
public function configureOptions(OptionsResolver $resolver)
{
if($this->entityManager->getRepository(DataCategory::class)->count([]) > 50) {
$resolver->setDefaults([
'attr' => [
'data-ajax' => '1',
'data-ajax-endpoint' => $this->router->generate('data-category-manage-select2')
],
'choices' => function (Options $options) {
$data = $options['data'];
dump('data', $data);
if($data !== null) {
if(is_array($data) || $data instanceof Collection){
$ret = [];
foreach ($data as $d) {
$ret[$d->description . ' (' . $d->name . ')'] = $d->getId()->toString();
}
dump($ret);
return $ret;
} else if ($data instanceof DataCategory){
return [$data->description . ' (' . $data->name . ')' => $data->getId()->toString()];
} else {
throw new \InvalidArgumentException("Argument unerwartet.");
}
} else {
return [];
}
}
]);
} else {
$resolver->setDefaults([
'class' => DataCategory::class,
'choice_label' => function ($cat, $key, $index) { return DataCategory::choiceLabel($cat);},
'choices' => function (Options $options) {
return $this->entityManager->getRepository(DataCategory::class)->getValidChildCategoryChoices($options['currentDataCategory']);
}
]);
}
}
}
It is very important to set the 'data'
option when using this new type, otherwise the choices option is not correctly set:
$builder->add('summands', Select2AjaxDataCategoryType::class,[
'currentDataCategory' => $mdc,
'data' => $mdc->summands->toArray(),
'multiple' => true,
'required' => false,
'label' => 'Summierte Kategorien',
]);
来源:https://stackoverflow.com/questions/57638447/dynamic-choicetype-select2-ajax