我需要一个表单字段来从数千个实体中进行选择,因此像 select2(使用 AJAX)这样的动态选择系统非常适合。
我的 AJAX 端点工作正常,但自定义表单类型不起作用:
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')
]
]);
}
}
使用这个表单类型的时候,似乎可以工作,但是最终没有返回实体对象,而是
null
。然而,根据 symfony 调试工具栏,收到了该值:
转储还表明视图和模型转换器被调用:
为了完整起见(我希望我们能找到一个完美的解决方案并帮助其他人),这是我的js代码(它有效):
$('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;
}
}
});
我已经解决了问题,这是我的完整解决方案:
$('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;
}
}
});
新的表单类型固定为一个类
DataCategory
,并且适用于单选和多选。
我在 select2 前端和标准 EntityType
之间建立了区别(主要是出于测试原因,因为新的基于 select2 的方法不允许使用 symfony 的 Client
(WebTestCase) 的 PHPUnit 测试):如果少于 50 个DB 中的 DataCategory 实体,该字段回退到 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']);
}
]);
}
}
}
使用这个新类型时设置
'data'
选项非常重要,否则choices选项设置不正确:
$builder->add('summands', Select2AjaxDataCategoryType::class,[
'currentDataCategory' => $mdc,
'data' => $mdc->summands->toArray(),
'multiple' => true,
'required' => false,
'label' => 'Summierte Kategorien',
]);